esbuild Bundler Configuration for Web Project
esbuild is written in Go and runs 10–100 times faster than JavaScript equivalents. A cold start that takes Webpack 30 seconds, esbuild completes in 300 ms. This changes not only wait times but development approach: rebuilds on every save stop being a problem.
Where esbuild is used directly
Most projects use esbuild indirectly — via Vite, which uses it for dev server, or via Jest with esbuild-jest. Direct use is justified when:
- you need a minimalist pipeline without framework overhead
- building Node.js server (bundling Lambda functions, CLI utilities)
- existing build is too slow and need targeted transpiler replacement
- monorepo project requires custom build script
Installation and first run
npm install --save-dev esbuild
Minimal run without config file:
./node_modules/.bin/esbuild src/index.ts \
--bundle \
--outfile=dist/bundle.js \
--target=es2020 \
--format=esm \
--sourcemap
JavaScript API
esbuild doesn't have YAML/JSON config — configuration via JS API or CLI. JS API gives full control:
// build.mjs
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
format: 'esm',
target: ['es2020', 'chrome90', 'firefox88', 'safari14'],
splitting: true, // code splitting for ESM
sourcemap: true,
minify: process.env.NODE_ENV === 'production',
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
external: ['react', 'react-dom'],
metafile: true, // for bundle analysis
loader: {
'.png': 'file',
'.svg': 'dataurl',
'.woff2': 'file',
},
});
Dev server with hot reload
esbuild has built-in dev server and watch mode:
// dev.mjs
import * as esbuild from 'esbuild';
const ctx = await esbuild.context({
entryPoints: ['src/index.tsx'],
bundle: true,
outdir: 'dist',
format: 'esm',
sourcemap: 'inline',
define: { 'process.env.NODE_ENV': '"development"' },
});
await ctx.watch();
const { host, port } = await ctx.serve({
servedir: 'public', // static files
port: 3000,
});
console.log(`Dev server: http://${host}:${port}`);
Built-in server supports SSE for live reload, but not HMR in React Fast Refresh sense. For full HMR, Vite is better.
Plugins
Plugin ecosystem is significantly smaller than Webpack, but key tasks are covered:
import { sassPlugin } from 'esbuild-sass-plugin';
import { copy } from 'esbuild-plugin-copy';
await esbuild.build({
plugins: [
sassPlugin({
type: 'css', // or 'css-module' for CSS Modules
}),
copy({
assets: [
{ from: './public/**/*', to: './dist' },
],
}),
],
});
Writing custom plugin is straightforward — API is simple:
const envPlugin: esbuild.Plugin = {
name: 'env',
setup(build) {
// Intercept imports like `import env from 'env'`
build.onResolve({ filter: /^env$/ }, (args) => ({
path: args.path,
namespace: 'env-ns',
}));
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify(process.env),
loader: 'json',
}));
},
};
Building for Node.js
Lambda functions, CLI tools, server scripts:
await esbuild.build({
entryPoints: ['src/handler.ts'],
bundle: true,
platform: 'node',
target: 'node18',
format: 'cjs', // Lambda expects CJS
outfile: 'dist/handler.js',
external: [
// AWS SDK not included — already in Lambda runtime
'@aws-sdk/*',
// Native modules
'bcrypt',
'sharp',
],
minify: true,
});
For native add-ons (.node files) esbuild won't work — need additional copy step.
Metafile analysis
const result = await esbuild.build({ ..., metafile: true });
const text = await esbuild.analyzeMetafile(result.metafile!, {
verbose: true,
});
console.log(text);
Or export to JSON and upload to esbuild.github.io/analyze/ — interactive sunburst diagram with module sizes.
Limitations
esbuild intentionally doesn't implement several things:
-
TypeScript type checking — esbuild only transpiles, doesn't check types. Run
tsc --noEmitseparately in CI -
CSS Modules — basic support, no
composesand custom class names like[local]_[hash] - Decorators — experimental support, old decorator syntax (emitDecoratorMetadata) not supported
- Some Babel transforms — if specific AST transforms are needed, use plugin or preprocess via SWC/Babel
Timeline
Replacing transpiler in existing project with esbuild (e.g., Jest → esbuild-jest): 2–4 hours. Setting up complete build pipeline from scratch (dev server, production build, CSS, assets): 4–8 hours.







