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

bash
# 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.

bash
# Global (acceptable — esbuild is small and self-contained)
npm install -g esbuild

Output: esbuild available system-wide.

bash
# One-off via npx
npx esbuild --bundle src/index.ts --outfile=dist/out.js

Output:

text
  dist/out.js  12.3kb

⚡ Done in 14ms

Versioning & Node support

  • esbuild lives on a 0.x line. As of writing the current minor is in the 0.20+ range. The 0.x line uses major-bumps on the minor — 0.200.21 may 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.0 release is "eventually" promised; in practice the 0.x cadence 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).

CompanionRole
esbuild-loaderwebpack loader for using esbuild as the transpiler.
@esbuild-kit/cjs-loader / @esbuild-kit/esm-loaderNode loaders that use esbuild — used internally by tsx (the package).
esbuild-plugin-*Community plugins (file resolution, glob imports, env var injection).
tsupWraps esbuild with library-friendly defaults.
unbuildSame role — wraps esbuild + rollup.

Alternatives

ToolTrade-off
swcRust transformer, comparable speed. Doesn't bundle by default; use SWC + a bundler.
RolldownRust port of Rollup. Larger feature surface than esbuild for library bundling.
RspackRust port of webpack. Use if you need webpack API + Rust speed.
oxcNewer Rust toolchain; transformer + linter + (eventual) bundler. Pre-1.0.
Bun's bundlerBuilt-in to Bun runtime. Fast; ties you to Bun.
tscTypeScript compiler. Type-checks; slow; no bundling. Use for .d.ts emit alongside esbuild.

Real-world recipes

Bundle a Node script (CLI)

bash
npx esbuild src/cli.ts \
  --bundle \
  --platform=node \
  --target=node20 \
  --outfile=dist/cli.cjs

Output:

text
  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)

typescript
import { transform } from "esbuild";

const result = await transform(
  "const x: number = 42; export default x;",
  { loader: "ts", target: "es2022" }
);

console.log(result.code);

Output:

text
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

bash
npx esbuild src/index.ts --bundle --outfile=dist/out.js --tsconfig=tsconfig.json

Output:

text
  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

bash
npx esbuild src/index.ts --bundle --minify --outfile=dist/out.min.js

Output:

text
  dist/out.min.js  4.7kb

⚡ Done in 18ms

Or finer-grained:

bash
npx esbuild src/index.ts --bundle \
  --minify-whitespace \
  --minify-identifiers \
  --minify-syntax \
  --outfile=dist/out.min.js

Output:

text
  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

bash
npx esbuild src/index.ts --bundle --sourcemap --outfile=dist/out.js

Output:

text
  dist/out.js      12.3kb
  dist/out.js.map  18.7kb

⚡ Done in 16ms

Emits dist/out.js.map alongside. Variants:

bash
--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

javascript
// 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",
  },
});
bash
node build.mjs

Output:

text
  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

bash
# Watch mode (rebuild on change)
npx esbuild src/index.ts --bundle --outfile=dist/out.js --watch

Output:

text
[watch] build started (initial)
[watch] build finished, watching for changes...
javascript
// 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 --target explicitly. Default is esnext (modern browsers + Node). For broader support, --target=es2020,chrome70,firefox68,safari12.
  • --minify shrinks output ~30%. Optional second pass with Terser for max compression.
  • --legal-comments=eof moves licence banners to the end of the file (legally sound, doesn't bloat the visible bundle top).
  • --metafile=meta.json emits build metadata for analysis (input → output mapping, sizes). Plug into esbuild.github.io/analyze for a treemap.
bash
npx esbuild src/index.ts \
  --bundle \
  --minify \
  --target=es2020 \
  --legal-comments=eof \
  --metafile=meta.json \
  --outfile=dist/out.js

Output:

text
  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

javascript
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

bash
npx esbuild ... --metafile=meta.json

Output:

text
  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:

bash
npx esbuild ... --sourcemap=external --sources-content=false

Output:

text
  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 → ToHighlights
0.16 → 0.17tsconfigRaw option type changed. serve API moved into context.
0.17 → 0.18Rename of certain loader and minify flags. Stricter handling of package.json exports.
0.18 → 0.19New --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

  1. 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_PATH to a pre-staged binary.
  2. define substitution is literal. --define:SECRET="\"abc\"" inlines the value into bundle source. Never --define server secrets.
  3. 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.
  4. --external mismatches. Marking a dep external but not actually providing it at runtime crashes the consumer. Pair with package.json peerDependencies.
  5. 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. Use npm install --include=optional for cross-platform CI Docker builds.

Testing & CI integration

yaml
# .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-loader instead of babel-loader for ~5–10× faster builds.
  • Rollup has rollup-plugin-esbuild for the same purpose.
  • tsx uses @esbuild-kit/esm-loader internally to transpile on-the-fly under Node.
  • Jest has @swc/jest and esbuild-jest as faster alternatives to babel-jest.

Troubleshooting common errors

[ERROR] Could not resolve "X"

esbuild's resolver couldn't find a package. Either:

  • Missing npm install X.
  • Need --platform=node to enable Node-style resolution (default is browser).
  • Path with @/ alias — esbuild reads tsconfig.json paths via --tsconfig, not automatically. Pass --tsconfig explicitly 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:

bash
npm install --include=optional
# or
npm rebuild esbuild

Output:

text
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.ts bundling. Rollup / tsup / unbuild handle multi-format, banner, and .d.ts more 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 --noEmit in CI.

See also