cheat sheet
esbuild
Package-level reference for esbuild on npm — bundle CLI, transform API, JS API, plugin system, and trade-offs vs Rollup, Webpack, and Vite.
esbuild
What it is
esbuild is a JavaScript bundler and transformer written in Go. It's the speed primitive of the modern JS toolchain — measured in milliseconds where webpack and Rollup measure in seconds. Vite uses it for dev-mode transpile and dep pre-bundling. tsup wraps it for library publishing. Most plugin-based bundlers either include esbuild as an internal speed boost or are slowly being replaced by Rust-based ports (Rolldown, Rspack, Turbopack) chasing similar performance.
esbuild is intentionally minimalist — it does bundling, transforming, and minification, and stops there. It does NOT have a rich plugin API (the plugin surface is deliberately small), does NOT do partial-evaluation magic (no process.env.NODE_ENV substitution unless you tell it), and does NOT replace webpack for huge plugin-driven apps. It's the right tool when speed matters more than ecosystem reach.
Install
# Project-local
npm install -D esbuild
pnpm add -D esbuild
yarn add -D esbuild
bun add -d esbuild
Output: esbuild binary in node_modules/.bin/. Note: esbuild downloads a platform-specific Go binary on npm install — the package on npm is a thin Node wrapper.
# Global (acceptable — esbuild is small and self-contained)
npm install -g esbuild
Output: esbuild available system-wide.
# One-off via npx
npx esbuild --bundle src/index.ts --outfile=dist/out.js
Output:
dist/out.js 12.3kb
⚡ Done in 14ms
Versioning & Node support
- esbuild lives on a
0.xline. As of writing the current minor is in the0.20+ range. The0.xline uses major-bumps on the minor —0.20→0.21may include breaking changes despite SemVer-on-paper. - Requires Node 12 or newer, but the underlying binary runs anywhere with a supported Go target (Linux, macOS, Windows, FreeBSD, on x64, arm64, and arm).
- Always pin to a specific minor version — pre-1.0 SemVer is loose.
- Pure binary install — no native build step from source.
- A
1.0release is "eventually" promised; in practice the0.xcadence is the de-facto stable surface.
Package metadata
- Maintainer: Evan Wallace (creator + sole primary maintainer)
- Project home: github.com/evanw/esbuild
- Docs: esbuild.github.io
- npm: npmjs.com/package/esbuild
- License: MIT
- First released: 2020
- Downloads: ~80M+ weekly — among the most-installed Node packages, mostly transitively via Vite and friends.
Peer dependencies & extras
esbuild has zero peers. The binary is self-contained — no Go toolchain needed, no compile-on-install step (it ships pre-built per platform via @esbuild/<platform>-<arch> optional deps).
| Companion | Role |
|---|---|
esbuild-loader | webpack loader for using esbuild as the transpiler. |
@esbuild-kit/cjs-loader / @esbuild-kit/esm-loader | Node loaders that use esbuild — used internally by tsx (the package). |
esbuild-plugin-* | Community plugins (file resolution, glob imports, env var injection). |
tsup | Wraps esbuild with library-friendly defaults. |
unbuild | Same role — wraps esbuild + rollup. |
Alternatives
| Tool | Trade-off |
|---|---|
| swc | Rust transformer, comparable speed. Doesn't bundle by default; use SWC + a bundler. |
| Rolldown | Rust port of Rollup. Larger feature surface than esbuild for library bundling. |
| Rspack | Rust port of webpack. Use if you need webpack API + Rust speed. |
| oxc | Newer Rust toolchain; transformer + linter + (eventual) bundler. Pre-1.0. |
| Bun's bundler | Built-in to Bun runtime. Fast; ties you to Bun. |
| tsc | TypeScript compiler. Type-checks; slow; no bundling. Use for .d.ts emit alongside esbuild. |
Real-world recipes
Bundle a Node script (CLI)
npx esbuild src/cli.ts \
--bundle \
--platform=node \
--target=node20 \
--outfile=dist/cli.cjs
Output:
dist/cli.cjs 1.2mb
⚡ Done in 47ms
--platform=node switches resolution from browser to Node (uses module.require, __dirname, etc.). --target=node20 controls syntax downleveling.
Transform API (no bundling)
import { transform } from "esbuild";
const result = await transform(
"const x: number = 42; export default x;",
{ loader: "ts", target: "es2022" }
);
console.log(result.code);
Output:
const x = 42;
export default x;
//# sourceMappingURL=...
transform operates on a single string — no filesystem reads, no resolution. Use for in-memory TS-to-JS compilation: REPLs, runtime evaluation, AST tooling that needs JS output.
Respect tsconfig.json
npx esbuild src/index.ts --bundle --outfile=dist/out.js --tsconfig=tsconfig.json
Output:
dist/out.js 18.4kb
⚡ Done in 21ms
esbuild reads compilerOptions.target, paths, experimentalDecorators, and useDefineForClassFields from tsconfig.json. Without --tsconfig, it falls back to defaults (no paths resolution, target=esnext).
Minification
npx esbuild src/index.ts --bundle --minify --outfile=dist/out.min.js
Output:
dist/out.min.js 4.7kb
⚡ Done in 18ms
Or finer-grained:
npx esbuild src/index.ts --bundle \
--minify-whitespace \
--minify-identifiers \
--minify-syntax \
--outfile=dist/out.min.js
Output:
dist/out.min.js 4.7kb
esbuild's minifier is ~10–100× faster than Terser, at the cost of some compression ratio (~5% less effective). For most apps the trade-off is fine; for libraries where every byte matters, run Terser as a second pass.
Sourcemap
npx esbuild src/index.ts --bundle --sourcemap --outfile=dist/out.js
Output:
dist/out.js 12.3kb
dist/out.js.map 18.7kb
⚡ Done in 16ms
Emits dist/out.js.map alongside. Variants:
--sourcemap # external .map file
--sourcemap=inline # base64-embedded in the JS
--sourcemap=external # external, no //# sourceMappingURL comment
--sourcemap=linked # external, with //# comment (default)
Output: (none — flag reference, not executable)
For production use external + upload to error tracker, OR linked + serve maps privately.
JS API for build pipelines
// build.mjs
import esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts", "src/worker.ts"],
bundle: true,
platform: "browser",
format: "esm",
outdir: "dist",
splitting: true,
target: ["es2022", "chrome110", "firefox110", "safari16"],
define: {
"process.env.NODE_ENV": '"production"',
},
loader: {
".png": "file",
".svg": "text",
},
});
node build.mjs
Output:
dist/index.js 12kb
dist/worker.js 8kb
dist/chunk-AB12.js 4kb ← shared chunk from splitting
⚡ Done in 89ms
The JS API is more flexible than the CLI — multi-entry, splitting, code transforms via the loader map.
Watch / serve modes
# Watch mode (rebuild on change)
npx esbuild src/index.ts --bundle --outfile=dist/out.js --watch
Output:
[watch] build started (initial)
[watch] build finished, watching for changes...
// JS API watch
const ctx = await esbuild.context({ /* options */ });
await ctx.watch();
// or serve with HTTP
const { host, port } = await ctx.serve({ servedir: "dist" });
console.log(`http://${host}:${port}`);
serve is a tiny dev server — file changes trigger rebuilds, but there's no HMR. Use Vite for SPA dev.
Production deployment
esbuild output is production-ready by default — it's a single Go binary; nothing to deploy with the artefact. Considerations:
- Set
--targetexplicitly. Default isesnext(modern browsers + Node). For broader support,--target=es2020,chrome70,firefox68,safari12. --minifyshrinks output ~30%. Optional second pass with Terser for max compression.--legal-comments=eofmoves licence banners to the end of the file (legally sound, doesn't bloat the visible bundle top).--metafile=meta.jsonemits build metadata for analysis (input → output mapping, sizes). Plug into esbuild.github.io/analyze for a treemap.
npx esbuild src/index.ts \
--bundle \
--minify \
--target=es2020 \
--legal-comments=eof \
--metafile=meta.json \
--outfile=dist/out.js
Output:
dist/out.js 47kb
meta.json 12kb
⚡ Done in 234ms
Performance tuning
esbuild is the speed baseline — there isn't much to tune.
Use incremental / context for repeated builds
const ctx = await esbuild.context({ /* options */ });
await ctx.rebuild(); // incremental rebuild
await ctx.rebuild(); // cached, instant
The CLI doesn't expose this; use the JS API for build-tool scripts that rebuild often.
Limit entryPoints granularity
A single entry point with code splitting is faster than many entry points without. esbuild parallelises within an entry but each top-level entry is sequential cost.
Profile with --metafile
npx esbuild ... --metafile=meta.json
Output:
dist/out.js 47kb
meta.json 12kb
⚡ Done in 32ms
Upload meta.json to the analyze tool to find unexpectedly large deps.
Skip sourcemap for production builds
The sourcemap pass adds ~30% to build time. Disable in CI:
npx esbuild ... --sourcemap=external --sources-content=false
Output:
dist/out.js 47kb
dist/out.js.map 68kb
⚡ Done in 21ms
--sources-content=false omits source content from the map — saves space; consumers can't see source unless you also publish original files.
Version migration guide
| From → To | Highlights |
|---|---|
| 0.16 → 0.17 | tsconfigRaw option type changed. serve API moved into context. |
| 0.17 → 0.18 | Rename of certain loader and minify flags. Stricter handling of package.json exports. |
| 0.18 → 0.19 | New --packages flag for marking certain imports external. Some plugin-API tweaks. |
| 0.19 → 0.20+ | Continued tweaks to target semantics; better tree-shaking heuristics; new minify options. |
Always read the CHANGELOG before bumping — pre-1.0 means real breaking changes on minor bumps. Most fixes-up are trivial in practice; pin and test.
A 1.0 release has been "soon" for years; current docs treat 0.x as the stable line.
Security considerations
- Platform binary install. esbuild's npm package downloads a pre-built Go binary per platform. The binary itself is small and signed; lockfile pins it. If you build offline, set
ESBUILD_BINARY_PATHto a pre-staged binary. definesubstitution is literal.--define:SECRET="\"abc\""inlines the value into bundle source. Never--defineserver secrets.- Source-map content embedding.
sourcesContent: true(default) includes original source in the map. Disable for libraries; the .map file then references file paths but doesn't ship code. --externalmismatches. Marking a depexternalbut not actually providing it at runtime crashes the consumer. Pair withpackage.jsonpeerDependencies.- Optional dep on missing platforms. esbuild lists every
@esbuild/<platform>as an optional dep. If you install on a platform without a published binary, the install succeeds but esbuild fails to run. Usenpm install --include=optionalfor cross-platform CI Docker builds.
Testing & CI integration
# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx tsc --noEmit # type-check
- run: npx esbuild src/index.ts --bundle --minify --outfile=dist/out.js
- run: ls -la dist/
- run: node dist/out.js # smoke-test
For libraries, pair esbuild (bundle) with tsc --emitDeclarationOnly (.d.ts) and publint (validate package shape).
Ecosystem integrations
- Vite uses esbuild for dev-mode transforms and for
optimizeDeps(pre-bundling CJS deps). Prod build uses Rollup. - tsup wraps esbuild with library defaults (multi-format output, .d.ts generation). Most modern small TS libraries use it.
- Storybook has an esbuild-loader option.
- Webpack can use
esbuild-loaderinstead ofbabel-loaderfor ~5–10× faster builds. - Rollup has
rollup-plugin-esbuildfor the same purpose. - tsx uses
@esbuild-kit/esm-loaderinternally to transpile on-the-fly under Node. - Jest has
@swc/jestandesbuild-jestas faster alternatives tobabel-jest.
Troubleshooting common errors
[ERROR] Could not resolve "X"
esbuild's resolver couldn't find a package. Either:
- Missing
npm install X. - Need
--platform=nodeto enable Node-style resolution (default isbrowser). - Path with
@/alias — esbuild readstsconfig.jsonpaths via--tsconfig, not automatically. Pass--tsconfigexplicitly or use a plugin.
Top-level await is not available in the configured target environment
esbuild's --target excludes top-level await. Bump target to es2022+, or refactor source to avoid TLA.
The CommonJS "require" function does not work in ESM scope
The output is ESM but source uses require(). Either:
- Convert source to
import. - Change output
--format=cjs. - Use
import { createRequire } from "module"; const require = createRequire(import.meta.url);shim.
Source maps don't show original source
Either --sources-content=false was set, or your IDE / browser is loading the bundle from a different URL than the sourceMappingURL expects. Use absolute paths in the sourcemap (--source-root).
Bundle includes node_modules/ deps when you expected externals
Default behaviour bundles all imports. Use --packages=external (esbuild 0.18+) to externalize every node_modules/ package, or list each: --external:express --external:react.
Binary fails to run after install
esbuild's platform-specific optional dep didn't install. Symptom: esbuild errors with Cannot find module '@esbuild/linux-x64'. Fix:
npm install --include=optional
# or
npm rebuild esbuild
Output:
added 3 packages, and audited 142 packages in 1s
When NOT to use this
- Apps with a plugin-rich pipeline. Vite / webpack give you HMR, CSS Modules, postcss, virtual modules, etc. esbuild's plugin API is intentionally small — extending it is more work than configuring Vite.
- Library publishing with multi-format +
.d.tsbundling. Rollup / tsup / unbuild handle multi-format, banner, and.d.tsmore ergonomically. esbuild is the low-level primitive; use a wrapper for libraries. - Maximum compression matters. esbuild's minifier trades ~5% bytes for ~10× speed. For tiny libraries shipped to billions, run Terser as a second pass.
- Older browser support. esbuild handles ES2020+ syntax downleveling; for full ES5 / IE11 polyfilling you need Babel + core-js or
@vitejs/plugin-legacy. - Type-checking matters at build time. esbuild strips types; it doesn't check them. Always pair with
tsc --noEmitin CI.
See also
- JavaScript: esbuild — CLI flags, transform API patterns
- JavaScript: modules — ESM, CJS, IIFE outputs
- Packages: npm-vite — esbuild-powered dev server
- Concept: HTTP — serving bundled assets