본문으로 건너뛰기
Previous
Next
esbuild Complete Guide | Fastest JavaScript Bundler

esbuild Complete Guide | Fastest JavaScript Bundler

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:

  1. Uses native code (Go compiles to machine code)
  2. Runs transformations in parallel
  3. Uses memory efficiently (no intermediate AST copies)

Benchmark (three.js library, from scratch):

ToolTime
esbuild0.10s
Rollup + terser22.9s
webpack41.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 tsc separately)
  • 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],
  // ...
})
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

esbuildVitewebpack
SpeedFastestFast (dev), Slower (prod)Slow
Use caseLibraries, CLI toolsWeb appsWeb apps (legacy/complex)
HMRManualBuilt-inBuilt-in
PluginsGrowing ecosystemLarge (Vite plugins + Rollup)Huge ecosystem
ConfigSimpleSimpleComplex
CSSBasicFull (PostCSS, Sass)Full
Code splittingESM onlyFullFull

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 --noEmit separately
  • 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 등으로 검색하시면 이 글이 도움이 됩니다.