esbuild & SWC — Fast Bundlers
Traditional JavaScript-based build tools are reaching their performance limits. esbuild (Go-based) and SWC (Rust-based) represent a new generation of bundlers and compilers that are 10-100x faster than their predecessors, leveraging systems-level languages for parallel processing and efficient memory management.
These tools are not just faster — they fundamentally change how build pipelines are architected. Modern frameworks like Vite, Next.js, and Turbopack use esbuild and SWC under the hood, making them invisible but essential infrastructure.
Created by Evan Wallace (Figma), esbuild is a JavaScript bundler and minifier written in Go. It achieves its speed through parallelism (using goroutines), efficient memory usage, and a from-scratch implementation that avoids the overhead of existing AST representations.
| 1 | # Install esbuild |
| 2 | npm install --save-dev esbuild |
| 3 | |
| 4 | # Basic usage — bundle a file |
| 5 | npx esbuild src/index.js --bundle --outfile=dist/bundle.js |
| 6 | |
| 7 | # Bundle with JSX and minification |
| 8 | npx esbuild src/app.tsx \ |
| 9 | --bundle \ |
| 10 | --outfile=dist/app.js \ |
| 11 | --loader:.tsx=tsx \ |
| 12 | --minify \ |
| 13 | --sourcemap \ |
| 14 | --target=es2020 |
| 15 | |
| 16 | # Development mode (no minify, with sourcemaps) |
| 17 | npx esbuild src/app.tsx --bundle --outfile=dist/app.js --sourcemap |
| Benchmark | esbuild | Webpack 5 | Rollup 4 | Parcel 2 |
|---|---|---|---|---|
| Bundle (10x React) | 0.36s | 15.9s | 8.2s | 7.8s |
| Minify (three.js) | 0.24s | 4.2s | — | — |
| TS -> JS (1000 files) | 0.48s | — | — | — |
pro tip
| 1 | // esbuild JavaScript API |
| 2 | const esbuild = require("esbuild"); |
| 3 | |
| 4 | // Build programmatically |
| 5 | await esbuild.build({ |
| 6 | entryPoints: ["src/index.tsx"], |
| 7 | outfile: "dist/bundle.js", |
| 8 | bundle: true, |
| 9 | minify: true, |
| 10 | sourcemap: true, |
| 11 | target: "es2020", |
| 12 | loader: { |
| 13 | ".tsx": "tsx", |
| 14 | ".svg": "dataurl", |
| 15 | ".png": "file", |
| 16 | }, |
| 17 | define: { |
| 18 | "process.env.NODE_ENV": '"production"', |
| 19 | }, |
| 20 | plugins: [/* ... */], |
| 21 | }); |
| 22 | |
| 23 | // Watch mode |
| 24 | const ctx = await esbuild.context({ |
| 25 | entryPoints: ["src/index.tsx"], |
| 26 | outfile: "dist/bundle.js", |
| 27 | bundle: true, |
| 28 | }); |
| 29 | await ctx.watch(); |
| 30 | console.log("Watching for changes..."); |
| 31 | |
| 32 | // Serve mode (dev server) |
| 33 | await ctx.serve({ |
| 34 | port: 3000, |
| 35 | servedir: "dist", |
| 36 | }); |
| 37 |
esbuild has a well-designed plugin API based on hooks that intercept the build lifecycle. While smaller than Webpack's ecosystem, the API is powerful and composable.
| 1 | const esbuild = require("esbuild"); |
| 2 | |
| 3 | // Custom plugin — strip console.log in production |
| 4 | const stripConsolePlugin = { |
| 5 | name: "strip-console", |
| 6 | setup(build) { |
| 7 | build.onLoad({ filter: /\.(js|ts)x?$/ }, (args) => { |
| 8 | // Transform on load |
| 9 | }); |
| 10 | |
| 11 | build.onEnd((result) => { |
| 12 | console.log(`Build finished with ${result.errors.length} errors`); |
| 13 | }); |
| 14 | }, |
| 15 | }; |
| 16 | |
| 17 | // Plugin for CSS modules |
| 18 | const cssModulesPlugin = { |
| 19 | name: "css-modules", |
| 20 | setup(build) { |
| 21 | build.onResolve({ filter: /\.module\.css$/ }, (args) => { |
| 22 | return { |
| 23 | path: path.resolve(args.resolveDir, args.path), |
| 24 | namespace: "css-module", |
| 25 | }; |
| 26 | }); |
| 27 | |
| 28 | build.onLoad({ filter: /.*/, namespace: "css-module" }, (args) => { |
| 29 | const css = readFileSync(args.path, "utf8"); |
| 30 | const classes = extractClassNames(css); |
| 31 | return { |
| 32 | contents: `export default ${JSON.stringify(classes)}`, |
| 33 | loader: "js", |
| 34 | }; |
| 35 | }); |
| 36 | }, |
| 37 | }; |
| 38 | |
| 39 | // Use plugins |
| 40 | await esbuild.build({ |
| 41 | entryPoints: ["src/index.tsx"], |
| 42 | bundle: true, |
| 43 | outfile: "dist/bundle.js", |
| 44 | plugins: [stripConsolePlugin, cssModulesPlugin], |
| 45 | }); |
| Hook | Purpose | Use Case |
|---|---|---|
| onResolve | Intercept module resolution | Custom import paths, virtual modules |
| onLoad | Intercept file loading | Transform content, CSS modules |
| onStart | Build start callback | Clear output, log timing |
| onEnd | Build end callback | Write manifest, notify services |
| onDispose | Cleanup on dispose | Close file watchers |
SWC (Speedy Web Compiler) is a Rust-based platform for transpilation, bundling, and minification. It serves as a drop-in replacement for Babel with 20x+ faster performance and is the compiler behind Next.js and Deno.
| 1 | # Install SWC CLI |
| 2 | npm install --save-dev @swc/cli @swc/core |
| 3 | |
| 4 | # Transpile a file |
| 5 | npx swc src/index.js --out-file dist/index.js |
| 6 | |
| 7 | # Watch mode |
| 8 | npx swc src -d dist -w |
| 9 | |
| 10 | # With configuration |
| 11 | npx swc src -d dist --config-file .swcrc |
| 1 | // .swcrc — SWC configuration |
| 2 | { |
| 3 | "jsc": { |
| 4 | "parser": { |
| 5 | "syntax": "typescript", |
| 6 | "tsx": true, |
| 7 | "decorators": true, |
| 8 | "dynamicImport": true |
| 9 | }, |
| 10 | "transform": { |
| 11 | "react": { |
| 12 | "runtime": "automatic", |
| 13 | "importSource": "react", |
| 14 | "refresh": true, |
| 15 | "pragma": "React.createElement" |
| 16 | }, |
| 17 | "optimizer": { |
| 18 | "globals": { |
| 19 | "vars": { |
| 20 | "process.env.NODE_ENV": "production" |
| 21 | } |
| 22 | } |
| 23 | } |
| 24 | }, |
| 25 | "target": "es2020", |
| 26 | "loose": false, |
| 27 | "externalHelpers": true, |
| 28 | "keepClassNames": true |
| 29 | }, |
| 30 | "module": { |
| 31 | "type": "es6", |
| 32 | "strict": true, |
| 33 | "strictMode": true, |
| 34 | "lazy": false, |
| 35 | "noInterop": false |
| 36 | }, |
| 37 | "minify": true, |
| 38 | "sourceMaps": true |
| 39 | } |
best practice
SWC can replace Babel in Webpack, Vite, and other build tools via dedicated loaders.
| 1 | // Webpack — swc-loader replaces babel-loader |
| 2 | module.exports = { |
| 3 | module: { |
| 4 | rules: [ |
| 5 | { |
| 6 | test: /\.(js|jsx|ts|tsx)$/, |
| 7 | exclude: /node_modules/, |
| 8 | use: { |
| 9 | loader: "swc-loader", |
| 10 | options: { |
| 11 | jsc: { |
| 12 | parser: { |
| 13 | syntax: "typescript", |
| 14 | tsx: true, |
| 15 | }, |
| 16 | transform: { |
| 17 | react: { runtime: "automatic" }, |
| 18 | }, |
| 19 | target: "es2020", |
| 20 | }, |
| 21 | }, |
| 22 | }, |
| 23 | }, |
| 24 | ], |
| 25 | }, |
| 26 | }; |
| 27 | |
| 28 | // With Next.js (next.config.js) |
| 29 | const nextConfig = { |
| 30 | swcMinify: true, |
| 31 | compiler: { |
| 32 | styledComponents: true, |
| 33 | removeConsole: process.env.NODE_ENV === "production", |
| 34 | }, |
| 35 | }; |
| 36 | |
| 37 | module.exports = nextConfig; |
info
Transpilation
Both tools excel at JSX/TypeScript to JavaScript transformation. SWC is the recommended choice for large TypeScript codebases where Babel has become a bottleneck.
| 1 | // SWC transpilation benchmark (1000 TypeScript files) |
| 2 | // Babel: ~45s |
| 3 | // SWC: ~2.1s (21x faster) |
| 4 | // esbuild: ~1.8s (25x faster) |
| 5 | // tsc: ~52s (without declaration generation) |
Minification
Both tools provide minifiers that are significantly faster than Terser while producing comparable output sizes.
| 1 | // esbuild minifier (direct replacement for Terser) |
| 2 | const esbuild = require("esbuild"); |
| 3 | |
| 4 | // Minify a single file |
| 5 | const result = await esbuild.build({ |
| 6 | entryPoints: ["src/index.js"], |
| 7 | minify: true, |
| 8 | allowOverwrite: true, |
| 9 | outfile: "src/index.js", |
| 10 | }); |
| 11 | |
| 12 | // SWC minification via API |
| 13 | const swc = require("@swc/core"); |
| 14 | const output = await swc.minify(code, { |
| 15 | compress: true, |
| 16 | mangle: true, |
| 17 | sourceMap: false, |
| 18 | }); |
| 19 | |
| 20 | // Both produce output ~5-10% larger than Terser |
| 21 | // but finish 10-100x faster |
Bundling (esbuild)
esbuild can be used as a standalone bundler for simple applications, libraries, and tools. It handles code splitting, CSS bundling, and asset loading.
| 1 | // Build a library with esbuild |
| 2 | await esbuild.build({ |
| 3 | entryPoints: ["src/index.ts"], |
| 4 | outfile: "dist/index.js", |
| 5 | format: "esm", |
| 6 | bundle: true, |
| 7 | minify: true, |
| 8 | sourcemap: true, |
| 9 | target: "es2020", |
| 10 | external: ["react", "react-dom"], |
| 11 | packages: "external", |
| 12 | }); |
| 13 | |
| 14 | // Bundling with CSS |
| 15 | await esbuild.build({ |
| 16 | entryPoints: ["src/index.tsx"], |
| 17 | outfile: "dist/bundle.js", |
| 18 | bundle: true, |
| 19 | loader: { |
| 20 | ".css": "css", |
| 21 | ".png": "file", |
| 22 | ".svg": "text", |
| 23 | }, |
| 24 | assetNames: "assets/[name]-[hash]", |
| 25 | }); |
While extremely fast, esbuild and SWC have important limitations that affect their suitability as full replacements for established tools.
| Limitation | esbuild | SWC | Impact |
|---|---|---|---|
| AST Modifications | Limited plugin hooks | Better but not Babel-level | Custom transforms harder |
| Plugin Ecosystem | Small (400+ plugins) | Small (200+ plugins) | Less community tooling |
| Type Checking | None (strips types) | None (strips types) | Requires separate tsc --noEmit |
| Code Splitting | Supported | Experimental | SWC bundling still maturing |
| Ecosystem Plugins | Polyfills, CSS-in-JS | Styled Components, Emotion | Some Babel plugins lack parity |
| ES Module Output | First-class | First-class | Both support ESM well |
warning
| Dimension | esbuild | SWC |
|---|---|---|
| Language | Go | Rust |
| Primary Use | Bundler + Minifier | Compiler + Transpiler |
| Bundling | Excellent (production-ready) | Experimental (v1.3+) |
| Transpilation | Good (no decorators, no legacy) | Excellent (full Babel parity) |
| Minification | Excellent (mature) | Excellent (mature) |
| Plugin API | Limited (onResolve/onLoad only) | Rich (Visitor-based AST transforms) |
| CSS Support | CSS bundling + minification | Experimental CSS modules |
| Adoption | Vite, ESM, Figma | Next.js, Deno, Parcel |
note
The most common way developers encounter these tools is through framework integrations. Here is how they compose with modern frameworks:
| 1 | Vite (development) |
| 2 | ├── esbuild ────────────────── Pre-bundling dependencies |
| 3 | │ ├── Convert CJS to ESM |
| 4 | │ ├── Bundle multi-file deps into single file |
| 5 | │ └── Support node_modules resolution |
| 6 | ├── esbuild ────────────────── Transpile TS/JSX |
| 7 | │ └── .ts, .tsx, .jsx files |
| 8 | └── Native ESM ─────────────── Serve to browser |
| 9 | |
| 10 | Vite (production) |
| 11 | ├── Rollup ─────────────────── Bundle application |
| 12 | └── esbuild ────────────────── Minify bundle |
| 13 | |
| 14 | Next.js |
| 15 | ├── SWC ────────────────────── Transpile pages & components |
| 16 | │ ├── JSX/TSX transformation |
| 17 | │ ├── Styled Components transform |
| 18 | │ ├── Remove console.log (configurable) |
| 19 | │ └── Modularize imports |
| 20 | ├── SWC ────────────────────── Minify JavaScript |
| 21 | └── Webpack / Turbopack ────── Bundle everything |
| 1 | // Vite — esbuild is used internally (no configuration needed) |
| 2 | import { defineConfig } from "vite"; |
| 3 | import react from "@vitejs/plugin-react"; |
| 4 | |
| 5 | export default defineConfig({ |
| 6 | optimizeDeps: { |
| 7 | // Control esbuild dep optimization |
| 8 | include: ["react", "react-dom", "lodash-es"], |
| 9 | exclude: ["@large-dep"], |
| 10 | esbuildOptions: { |
| 11 | target: "es2020", |
| 12 | }, |
| 13 | }, |
| 14 | }); |
| 15 | |
| 16 | // Next.js — SWC is the default compiler (no Babel needed) |
| 17 | // next.config.js |
| 18 | const nextConfig = { |
| 19 | // SWC is already the default |
| 20 | // Only disable if you have a .babelrc |
| 21 | swcMinify: true, |
| 22 | compiler: { |
| 23 | // Built-in SWC transforms |
| 24 | styledComponents: true, |
| 25 | emotion: true, |
| 26 | relay: { |
| 27 | src: "./src", |
| 28 | language: "typescript", |
| 29 | }, |
| 30 | removeConsole: { |
| 31 | exclude: ["error"], |
| 32 | }, |
| 33 | }, |
| 34 | }; |
best practice