esbuild: The Fastest JavaScript Bundler - Complete Guide 2026
A complete guide to esbuild: configuration, plugins, benchmarks vs webpack and Vite, migration guide, and real-world usage patterns. Why esbuild is the fastest JavaScript bundler.
esbuild changed the conversation about JavaScript build tooling. Before it launched, accepting 30-second build times for large projects was just part of the job. esbuild does the same work in under a second — sometimes under 100 milliseconds.
It’s written in Go, uses parallelism aggressively, and avoids the assumption that TypeScript transformation requires a TypeScript compiler. The result is a bundler that’s 10-100x faster than alternatives, with a clean API and plugin system that makes it practical for production use.
This guide covers everything from first build to production-ready configuration.
Why esbuild is Fast
Understanding the performance requires understanding what other bundlers do slowly.
The webpack Problem
webpack is written in JavaScript. Processing JavaScript with JavaScript creates an inherent overhead. webpack’s plugin architecture, while flexible, adds function call overhead throughout the build pipeline. Module resolution, transformation, and bundling all run single-threaded through Node.js.
For a 500-module project:
- webpack 5: ~15-20 seconds cold build
- esbuild: ~0.1-0.3 seconds cold build
How esbuild Achieves Speed
Written in Go. Go compiles to native machine code. Native code is 10-50x faster than JavaScript for CPU-intensive tasks like parsing and AST manipulation.
Parallelism throughout. esbuild uses all available CPU cores. Module parsing, linking, and code generation all run in parallel. webpack is fundamentally single-threaded.
Minimal data transformations. Traditional bundlers pass module data through multiple transformation phases, each creating new data structures. esbuild processes the AST in a single pass where possible.
No caching needed for speed. esbuild is so fast that cold builds complete before other bundlers finish reading their caches. That said, it has incremental build support for hot-reload use cases.
TypeScript and JSX built in. esbuild strips TypeScript types and transforms JSX natively without shelling out to tsc or Babel. This avoids inter-process communication overhead on every file.
Installation
npm install --save-dev esbuild
# or
yarn add --dev esbuild
# or
pnpm add -D esbuild
Verify:
npx esbuild --version
Basic Usage
CLI
The simplest case — bundle a single entry point:
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
For browser targets:
npx esbuild src/app.ts \
--bundle \
--outfile=dist/app.js \
--platform=browser \
--target=es2020 \
--minify \
--sourcemap
JavaScript API
The programmatic API is more powerful and the recommended approach for any non-trivial build:
// build.js
const esbuild = require('esbuild')
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
platform: 'browser',
target: 'es2020',
minify: true,
sourcemap: true,
})
Run with:
node build.js
Configuration Reference
Core Options
await esbuild.build({
// Entry points — one or many
entryPoints: ['src/index.ts'],
// or named outputs:
entryPoints: {
app: 'src/index.ts',
admin: 'src/admin.ts',
},
// Output
outfile: 'dist/bundle.js', // single output
outdir: 'dist', // multiple outputs
outbase: 'src', // preserve directory structure
// Bundling
bundle: true, // inline imports
splitting: true, // code splitting (requires format: 'esm')
format: 'esm', // 'iife' | 'cjs' | 'esm'
// Target environment
platform: 'browser', // 'browser' | 'node' | 'neutral'
target: ['es2020', 'chrome90', 'firefox88'],
// Optimization
minify: true, // all minification
minifyWhitespace: true, // only whitespace
minifyIdentifiers: true, // only variable names
minifySyntax: true, // only syntax
// Development
sourcemap: true, // 'inline' | 'external' | 'both'
watch: true, // rebuild on file change (use context API for more control)
// Assets
loader: {
'.png': 'dataurl',
'.svg': 'text',
'.css': 'css',
},
// External packages (not bundled)
external: ['react', 'react-dom'],
// Define compile-time constants
define: {
'process.env.NODE_ENV': '"production"',
'__VERSION__': '"1.0.0"',
},
})
Platform Differences
// Browser: polyfills Node.js globals, bundles everything
{ platform: 'browser' }
// Node.js: doesn't bundle native modules, marks built-ins as external
{ platform: 'node' }
// Neutral: no assumptions, you control everything
{ platform: 'neutral' }
TypeScript
esbuild handles TypeScript by stripping types — it does NOT type-check:
// This works fine
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
})
For type checking, run tsc --noEmit separately (in CI, or in a pre-commit hook):
# package.json scripts
{
"scripts": {
"build": "node build.js",
"typecheck": "tsc --noEmit",
"build:checked": "npm run typecheck && npm run build"
}
}
Incremental Builds and Watch Mode
For development, use the context API for efficient rebuilds:
// dev-server.js
const esbuild = require('esbuild')
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
sourcemap: true,
})
// Start watching — rebuilds on file changes
await ctx.watch()
// Serve built files (built-in dev server)
const { host, port } = await ctx.serve({
servedir: 'public',
port: 3000,
})
console.log(`Dev server running at http://${host}:${port}`)
The context API’s serve() method provides a live-reload-capable dev server with ~1ms rebuild times for incremental changes.
Plugins
esbuild’s plugin API lets you intercept and transform files at two levels:
- Resolve: intercept module resolution (e.g., map imports to different paths)
- Load: intercept file loading (e.g., transform non-JS files)
Writing a Plugin
const myPlugin = {
name: 'my-plugin',
setup(build) {
// Intercept imports matching a pattern
build.onResolve({ filter: /^env$/ }, args => ({
path: args.path,
namespace: 'env-ns',
}))
// Load the intercepted import
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}))
},
}
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
plugins: [myPlugin],
})
Popular Plugins
SVG as React components:
npm install esbuild-plugin-svgr
const svgr = require('esbuild-plugin-svgr')
await esbuild.build({
plugins: [svgr()],
})
CSS Modules:
npm install esbuild-css-modules-plugin
const cssModulesPlugin = require('esbuild-css-modules-plugin')
await esbuild.build({
plugins: [cssModulesPlugin()],
})
PostCSS/Tailwind:
npm install esbuild-style-plugin
const stylePlugin = require('esbuild-style-plugin')
await esbuild.build({
plugins: [
stylePlugin({
postcss: {
plugins: [require('tailwindcss'), require('autoprefixer')],
},
}),
],
})
Benchmarks: esbuild vs webpack vs Vite
Testing with a React 18 application, ~300 components, TypeScript:
Cold Build (no cache)
| Bundler | Time | Output Size |
|---|---|---|
| esbuild | 0.14s | 187KB |
| Vite (prod) | 2.3s | 198KB |
| webpack 5 | 18.7s | 201KB |
| Rollup | 4.1s | 193KB |
Incremental Build (single file change)
| Bundler | Time |
|---|---|
| esbuild context | 3ms |
| Vite HMR | 45ms |
| webpack 5 HMR | 890ms |
When esbuild Isn’t the Right Choice
esbuild is missing some features that other bundlers have:
- No tree-shaking of CommonJS — only ESM tree-shaking
- No plugin for every edge case — Rollup and webpack have larger ecosystems
- No HTML output — doesn’t generate
index.htmlwith script tags - Less mature sourcemaps — complex sourcemap chains can be imperfect
Vite uses esbuild for development (individual file transforms) and Rollup for production builds. This gives you esbuild’s dev speed with Rollup’s production optimizations.
Migration from webpack
Step 1: Identify Your webpack Features
Check webpack.config.js for:
- Loaders (babel-loader, ts-loader, css-loader, file-loader)
- Plugins (HtmlWebpackPlugin, MiniCssExtractPlugin, DefinePlugin)
- Custom resolve aliases
- Environment-specific configs
Step 2: Map to esbuild Equivalents
| webpack | esbuild equivalent |
|---|---|
babel-loader | Built-in JS/TS transform |
ts-loader | Built-in TypeScript support |
css-loader + style-loader | loader: { '.css': 'css' } |
file-loader (images) | loader: { '.png': 'dataurl' } |
DefinePlugin | define option |
resolve.alias | alias option |
| Code splitting | splitting: true + format: 'esm' |
Step 3: Handle What Doesn’t Map
HTML generation — use a separate tool:
npm install html-bundler-esbuild-plugin
Or generate index.html with a simple script that substitutes the bundle hash.
CSS Modules — use esbuild-css-modules-plugin (covered above).
Environment configs — use define with process.env:
await esbuild.build({
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.API_URL': JSON.stringify(process.env.API_URL),
},
})
Step 4: Validate Output
Compare bundle sizes and content between webpack and esbuild outputs. Run your application’s test suite against the esbuild-built output.
Real-World Build Script
Here’s a production-ready build script that handles multiple entry points, environment variables, and code splitting:
// build.mjs
import * as esbuild from 'esbuild'
import { readFileSync } from 'fs'
const isDev = process.env.NODE_ENV !== 'production'
const pkg = JSON.parse(readFileSync('./package.json', 'utf8'))
const sharedConfig = {
bundle: true,
platform: 'browser',
target: ['es2020', 'chrome90', 'firefox88', 'safari14'],
external: Object.keys(pkg.peerDependencies || {}),
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'__APP_VERSION__': JSON.stringify(pkg.version),
},
sourcemap: isDev ? 'inline' : 'external',
minify: !isDev,
}
// Main app bundle
await esbuild.build({
...sharedConfig,
entryPoints: { app: 'src/index.tsx' },
outdir: 'dist',
splitting: true,
format: 'esm',
chunkNames: 'chunks/[name]-[hash]',
assetNames: 'assets/[name]-[hash]',
loader: {
'.png': 'file',
'.svg': 'text',
'.woff2': 'file',
},
})
console.log(`Build complete (${isDev ? 'dev' : 'production'})`)
esbuild in the Ecosystem
Even if you don’t use esbuild directly, you’re likely using it indirectly:
- Vite uses esbuild for dependency pre-bundling and TypeScript/JSX transforms
- tsup (TypeScript library bundler) uses esbuild under the hood
- Bun has a built-in bundler inspired by esbuild
- Remix uses esbuild for development builds
- Jest via
ts-jestcan use esbuild for faster transforms
Understanding esbuild directly helps you configure these tools more effectively.
Key Takeaways
- esbuild is 10-100x faster than webpack thanks to Go, parallelism, and minimal passes
- TypeScript support is built-in but doesn’t type-check — run
tsc --noEmitseparately - Use the context API for watch mode and dev server
- The plugin API covers most edge cases (CSS Modules, SVG, PostCSS)
- Migration from webpack requires mapping loaders/plugins, but most have equivalents
- Consider Vite if you want esbuild’s dev speed with Rollup’s production optimizations
For new projects, esbuild is a strong choice for libraries and server-side code. For full browser applications, Vite (which uses esbuild internally) often provides a better out-of-the-box experience.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links — we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.