Getting Started with esbuild
Overview
esbuild is a JavaScript bundler and minifier written in Go. It parses, transforms, and打包 JavaScript in a fraction of the time that JavaScript-based bundlers require. A build that takes webpack 30 seconds can take esbuild under a second.
The speed comes from two things: Go runs natively, and esbuild processes your code in parallel using all available CPU cores. It is not a development server with HMR — it is a build tool. For dev server workflows, you would typically pair esbuild with a tool like Vite (which uses esbuild internally for its transformation step).
esbuild handles TypeScript, JSX, tree shaking, bundling, and minification out of the box. It does not have a plugin system as powerful as Rollup’s, but what it does, it does extremely fast.
Installation
npm install -D esbuild
The package installs both the CLI and the Node.js API.
Using the CLI
Basic bundling
esbuild src/index.js --bundle --outfile=dist/bundle.js
--bundle tells esbuild to follow imports and bundle everything into one file. Without --bundle, esbuild only transforms the single file you give it.
With minification and sourcemaps
esbuild src/index.js \
--bundle \
--outfile=dist/bundle.js \
--minify \
--sourcemap
Target a specific JavaScript version
esbuild src/index.js --bundle --outfile=dist/bundle.js --target=es2020
--target tells esbuild what syntax to preserve and what to transform. Older targets cause more transformation work but produce wider compatibility.
Multiple entry points
esbuild src/index.js src/admin.js --bundle --outdir=dist
This produces dist/index.js and dist/admin.js.
Watch mode
esbuild src/index.js --bundle --outfile=dist/bundle.js --watch
esbuild watches the input files and rebuilds whenever they change.
The Node.js API
For more control, call esbuild programmatically:
const esbuild = require('esbuild');
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
target: ['es2020'],
});
esbuild.build() returns a promise that resolves when the build is complete. It throws on errors.
Get transformed output without bundling
const { outputFiles } = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: false,
write: false,
});
console.log(outputFiles[0].text); // the transformed JavaScript
write: false prevents esbuild from writing to disk — it returns the result as an object instead.
Inspect build warnings and errors
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
});
if (result.errors.length > 0) {
console.error(result.errors);
}
TypeScript and JSX
esbuild strips TypeScript types and transforms JSX without needing a separate TypeScript or Babel setup:
esbuild src/index.tsx --bundle --outfile=dist/bundle.js --loader:.tsx=tsx --loader:.ts=ts
In the Node.js API:
await esbuild.build({
entryPoints: ['src/index.tsx'],
bundle: true,
outfile: 'dist/bundle.js',
loader: { '.tsx': 'tsx', '.ts': 'ts' },
});
esbuild does not perform type checking — it only strips types. Run tsc --noEmit in your CI pipeline to catch type errors.
JSX configuration
By default, esbuild transforms JSX into React.createElement calls. You can change this behavior:
await esbuild.build({
entryPoints: ['src/index.tsx'],
jsx: 'automatic', // transforms to React.createElement (React 17+)
jsxImportSource: 'preact', // use preact's JSX factory instead
});
Code Splitting
esbuild supports code splitting via dynamic imports:
// src/index.js
const { heavy } = await import('./heavy.js');
heavy.run();
esbuild src/index.js --bundle --outdir=dist --splitting --format=esm
--splitting works with --format=esm. esbuild creates a separate chunk for heavy.js and updates the import URL in the output.
For CommonJS output, use --format=cjs. Note that code splitting with CommonJS requires a runtime loader like Rollup-plugin-commonjs or a custom setup — esbuild does not handle the runtime aspect for CJS.
Tree Shaking
esbuild removes unused code automatically. By default it assumes that code with side effects might be needed, even if not imported:
// This unused function is removed by esbuild
function unusedHelper() {
return 'not referenced';
}
To explicitly mark code as having no side effects, use /* @__PURE__ */ or /* #__PURE__ */:
const result = /* @__PURE__ */ someCall();
esbuild treats this as side-effect-free and removes someCall() if its result is not used.
Plugins
esbuild’s plugin API lets you handle files that esbuild does not support natively:
const esbuild = require('esbuild');
const { existsSync, readFileSync } = require('fs');
const svgPlugin = {
name: 'svg',
setup(build) {
build.onLoad({ filter: /\.svg$/ }, async (args) => {
const contents = readFileSync(args.path, 'utf8');
return {
contents: `export default ${JSON.stringify(contents)}`,
loader: 'js',
};
});
},
};
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
plugins: [svgPlugin],
});
This plugin imports SVG files as string exports, which is useful for icon systems.
Official plugins
esbuild-plugin-postcss— CSS modules and PostCSS@esbuild/plugin-eslint— ESLint integration@esbuild/plugin-typescript— explicit TypeScript handling
Common Use Cases
Building a library
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
format: 'esm',
platform: 'node',
target: 'node18',
});
Use platform: 'node' to set correct tree-shaking behavior for Node.js and prevent bundling of Node.js built-ins like fs and path.
Building for browsers
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
format: 'iife', // immediately-invoked function expression
minify: true,
target: 'es2020',
});
iife format wraps the output in an IIFE so the bundle is safe to include via a <script> tag.
Re-building on changes
const ctx = await esbuild.context({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
sourcemap: true,
});
await ctx.watch();
console.log('Watching for changes...');
context() returns a context object with watch() and rebuild() methods. watch() continuously watches. rebuild() does a single rebuild.
Performance Comparison
esbuild is consistently the fastest JS bundler in benchmarks:
| Bundler | Cold build (large project) |
|---|---|
| esbuild | ~0.5s |
| Rollup | ~8s |
| webpack | ~25s |
| Parcel | ~12s |
These are rough figures for a typical real-world project with many modules. Your mileage varies, but the speed advantage is significant whenever incremental builds matter — in watch mode, in CI pipelines, or during development.
Gotchas
No HMR. esbuild is a bundler, not a dev server. Use it with Vite (which uses esbuild for transformations), a simple static server, or a custom HMR setup. Do not expect esbuild --watch to update your browser automatically.
Single config file. Unlike webpack which has complex configuration, esbuild configuration lives in the CLI flags or the build API call. For complex projects, put the build call in a build.js file and run it with node build.js.
Limited plugin ecosystem. Rollup’s plugin system is more powerful and has more plugins available. esbuild’s plugin API covers most needs but some advanced use cases require workarounds or a different tool.
No hot reloading by default. Pair with a dev server for that experience. esbuild’s --serve flag serves files but does not inject updates — it is not HMR.
Watch mode does not handle symlinks correctly on some platforms. If your project uses symlinked node_modules, watch mode may miss changes. Restart the build or use a different tool for watch workflows in monorepos.
See Also
- /guides/javascript-npm-workspaces/ — npm workspaces that pair with esbuild for monorepo builds
- /guides/javascript-modules-esm/ — ESM syntax that esbuild handles natively
- /guides/javascript-node-best-practices/ — Node.js build tooling conventions