esbuild Complete Guide | Fastest JavaScript Bundler
이 글의 핵심
esbuild bundles JavaScript and TypeScript 10–100x faster than webpack or Rollup by writing the bundler in Go instead of JavaScript. This guide covers the CLI, Node.js API, plugins, and production patterns.
Why esbuild?
esbuild is a JavaScript/TypeScript bundler written in Go. It’s fast because it:
- Uses native code (Go compiles to machine code)
- Runs transformations in parallel
- Uses memory efficiently (no intermediate AST copies)
Benchmark (three.js library, from scratch):
| Tool | Time |
|---|---|
| esbuild | 0.10s |
| Rollup + terser | 22.9s |
| webpack | 41.6s |
Real-World Adoption
esbuild has revolutionized JavaScript build tooling:
Production Usage:
- Vite: Uses esbuild for dependency pre-bundling (Evan You’s framework)
- Remix: Employs esbuild for fastest possible build times
- Phoenix Framework: Integrates esbuild as default asset bundler (Elixir)
- Cloudflare Workers: Uses esbuild for edge deployment builds
- Snowpack: Built on esbuild for instant development
Market Impact:
- 15+ million weekly npm downloads (April 2026)
- 38,000+ GitHub stars - massive adoption for a 2020 project
- Created by Evan Wallace (Figma co-founder) - wrote it to solve Figma’s build problems
- Used in 500,000+ repositories in just 4 years
Why esbuild Transformed Tooling:
- 10-100x faster: What took 30s in webpack takes 0.3s in esbuild
- Written in Go: Parallelizes everything, no JavaScript overhead
- Zero config: TypeScript, JSX, minification work out of the box
- Tree shaking: Fast dead code elimination for ES modules
Framework Integration:
- Vite (11M downloads/week): esbuild for deps + TS transpilation
- SvelteKit: esbuild for production optimization
- Astro: Uses esbuild for build pipeline
- Remix: Default bundler option
Performance Numbers:
- 0.10s to bundle Three.js (41s in webpack)
- 0.01s TypeScript transpilation for large codebases
- Parallel by default: Uses all CPU cores automatically
Limitations (by design):
- No type checking: Intentionally skips type checking for speed (run
tscseparately) - Basic tree shaking: Faster but less aggressive than Rollup
- Plugin ecosystem: Smaller than Webpack (but growing fast)
When to Choose esbuild:
- ✅ Library bundling (fastest option)
- ✅ Development builds (through Vite)
- ✅ CLI tools (instant startup)
- ✅ TypeScript transpilation only
- ❌ Complex Webpack migrations (use Vite/Rollup)
- ❌ Need deep tree shaking (use Rollup)
esbuild proved that build tools didn’t have to be slow - its success pushed the entire ecosystem to prioritize speed.
Installation
# npm
npm install --save-dev esbuild
# Check version
npx esbuild --version
1. CLI — Quick Start
# Bundle a single file
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
# With source maps and minification
npx esbuild src/index.ts \
--bundle \
--minify \
--sourcemap \
--target=chrome90 \
--outfile=dist/bundle.js
# Watch mode
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js --watch
# Multiple entry points
npx esbuild src/main.ts src/worker.ts --bundle --outdir=dist/
# Platform targets
npx esbuild src/server.ts --bundle --platform=node --outfile=dist/server.js
# Format options: iife, cjs, esm
npx esbuild src/lib.ts --bundle --format=esm --outfile=dist/lib.mjs
2. Node.js API
The API is more flexible than the CLI for complex builds.
Basic build
// build.mjs
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
minify: true,
sourcemap: true,
target: ['chrome90', 'firefox90', 'safari15'],
outfile: 'dist/bundle.js',
})
console.log('Build complete')
Multiple entry points with code splitting
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: {
main: 'src/main.ts',
worker: 'src/worker.ts',
'vendor/react': 'react', // extract to separate chunk
},
bundle: true,
splitting: true, // enable code splitting (ESM only)
format: 'esm',
outdir: 'dist',
chunkNames: 'chunks/[name]-[hash]',
})
Production build for Node.js CLI
import * as esbuild from 'esbuild'
import { execSync } from 'child_process'
await esbuild.build({
entryPoints: ['src/cli.ts'],
bundle: true,
platform: 'node',
target: 'node20',
format: 'cjs',
outfile: 'dist/cli.js',
external: [
// Don't bundle these — they'll be installed as dependencies
'fsevents',
],
banner: {
js: '#!/usr/bin/env node', // shebang for CLI
},
minify: true,
sourcemap: true,
})
3. Watch Mode + Dev Server
import * as esbuild from 'esbuild'
import { createServer } from 'http'
import { readFile } from 'fs/promises'
// Watch mode — rebuild on change
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
sourcemap: true,
outdir: 'dist',
})
await ctx.watch()
console.log('Watching for changes...')
// Simple dev server
const server = createServer(async (req, res) => {
const filePath = req.url === '/' ? '/index.html' : req.url
try {
const content = await readFile(`dist${filePath}`)
res.writeHead(200)
res.end(content)
} catch {
res.writeHead(404)
res.end('Not found')
}
})
server.listen(3000, () => console.log('Dev server: http://localhost:3000'))
// Cleanup on exit
process.on('SIGINT', async () => {
await ctx.dispose()
server.close()
process.exit(0)
})
esbuild’s built-in serve (simpler)
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
})
// Built-in serve — handles file watching + live reload
const { host, port } = await ctx.serve({
servedir: 'dist',
port: 3000,
onRequest: ({ remoteAddress, method, path, status }) => {
console.log(`${method} ${path} → ${status}`)
},
})
console.log(`Serving at http://${host}:${port}`)
4. Plugins
Plugins let you intercept the resolve and load phases:
// Plugin to handle CSS Modules
const cssModulesPlugin = {
name: 'css-modules',
setup(build) {
build.onLoad({ filter: /\.module\.css$/ }, async (args) => {
const css = await readFile(args.path, 'utf8')
// Transform CSS modules → JS object
const { transformed, classNames } = transformCSSModules(css)
return {
contents: `
const style = document.createElement('style')
style.textContent = ${JSON.stringify(transformed)}
document.head.appendChild(style)
export default ${JSON.stringify(classNames)}
`,
loader: 'js',
}
})
},
}
// Plugin to replace environment variables
const envPlugin = {
name: 'env',
setup(build) {
build.onResolve({ filter: /^env$/ }, (args) => ({
path: args.path,
namespace: 'env-ns',
}))
build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
contents: JSON.stringify({
NODE_ENV: process.env.NODE_ENV,
API_URL: process.env.API_URL,
}),
loader: 'json',
}))
},
}
// Usage in build
await esbuild.build({
plugins: [cssModulesPlugin, envPlugin],
// ...
})
Popular community plugins
import { sassPlugin } from 'esbuild-sass-plugin'
import svgrPlugin from 'esbuild-plugin-svgr'
import { copy } from 'esbuild-plugin-copy'
await esbuild.build({
plugins: [
sassPlugin(), // SCSS → CSS
svgrPlugin(), // SVG → React component
copy({
assets: { from: ['public/**/*'], to: ['dist'] }
}),
],
})
5. TypeScript — Transpile Without Bundle
// Transpile TS → JS without bundling (like tsc but fast)
await esbuild.build({
entryPoints: ['src/**/*.ts'], // all TS files
outdir: 'dist',
format: 'esm',
// NO bundle: true → preserves import statements
})
// Fast type checking separate from bundling
// package.json
{
"scripts": {
"build": "node build.mjs",
"typecheck": "tsc --noEmit",
"build:full": "npm run typecheck && npm run build"
}
}
tsconfig integration
// esbuild reads tsconfig.json automatically
// But you can specify it explicitly
await esbuild.build({
entryPoints: ['src/index.ts'],
tsconfig: './tsconfig.build.json', // separate tsconfig for builds
})
6. Library Build (Dual ESM/CJS)
// build.mjs — build for npm library
import * as esbuild from 'esbuild'
const shared = {
entryPoints: ['src/index.ts'],
bundle: true,
external: ['react', 'react-dom'], // peer deps — don't bundle
sourcemap: true,
}
// ESM build
await esbuild.build({
...shared,
format: 'esm',
outfile: 'dist/index.mjs',
})
// CommonJS build
await esbuild.build({
...shared,
format: 'cjs',
outfile: 'dist/index.cjs',
})
// Type declarations (still need tsc for this)
// tsc --emitDeclarationOnly --declaration --outDir dist/types
// package.json
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/types/index.d.ts"
}
}
}
7. Transform API
For single-file transforms without bundling:
import * as esbuild from 'esbuild'
// Transform TypeScript string to JavaScript
const result = await esbuild.transform(`
const greet = (name: string): string => \`Hello, \${name}!\`
export default greet
`, {
loader: 'ts',
target: 'es2020',
minify: true,
})
console.log(result.code)
// const greet=n=>`Hello, ${n}!`;export default greet;
// Transform JSX
const jsx = await esbuild.transform('<div className="hello">Hello</div>', {
loader: 'jsx',
jsxFactory: 'h',
jsxFragment: 'Fragment',
})
8. Define and Inject
await esbuild.build({
// Replace global constants at build time
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'__VERSION__': JSON.stringify('1.2.3'),
'DEBUG': 'false',
},
// Inject variables into every file
inject: ['./src/polyfills.js'],
})
esbuild vs Vite vs webpack
| esbuild | Vite | webpack | |
|---|---|---|---|
| Speed | Fastest | Fast (dev), Slower (prod) | Slow |
| Use case | Libraries, CLI tools | Web apps | Web apps (legacy/complex) |
| HMR | Manual | Built-in | Built-in |
| Plugins | Growing ecosystem | Large (Vite plugins + Rollup) | Huge ecosystem |
| Config | Simple | Simple | Complex |
| CSS | Basic | Full (PostCSS, Sass) | Full |
| Code splitting | ESM only | Full | Full |
Decision guide:
- Building a npm library → esbuild (fast, simple dual ESM/CJS)
- Building a CLI tool → esbuild (zero-config Node.js bundle)
- Building a web app → Vite (uses esbuild internally + better DX)
- Legacy app with many webpack plugins → webpack
Key Takeaways
- esbuild is 10–100x faster than webpack/Rollup by using Go and parallelism
- Transpiles TypeScript but does NOT type-check — run
tsc --noEmitseparately - API modes:
build()for files,transform()for strings,context()for watch mode - Plugins hook into resolve/load phases — handles SCSS, SVG, environment variables
- Code splitting requires
format: 'esm'+splitting: true - Best for libraries and Node.js tools; use Vite for full-featured web app DX
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Bundle and transform JavaScript/TypeScript at native speed. Covers esbuild CLI, Node.js API, plugins, watch mode, code s… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [Vite Complete Guide | Fast Dev Server· Build Optimization](/en/blog/vite-complete-guide/
- [Webpack Complete Guide | Internals· Loaders](/en/blog/webpack-complete-guide/
- [TypeScript 5 Complete Guide | Decorators· satisfies](/en/blog/typescript-5-complete-guide-en/
이 글에서 다루는 키워드 (관련 검색어)
esbuild, Build Tools, JavaScript, TypeScript, Performance, Frontend 등으로 검색하시면 이 글이 도움이 됩니다.