esbuild and SWC: Native Speed
The Speed Problem
JavaScript build tools have a dirty secret: they're slow because they're written in JavaScript.
Babel, the workhorse of the JavaScript ecosystem for years, is written in JavaScript. It parses your code into an AST (Abstract Syntax Tree), walks that tree, applies transforms, and generates output — all in a single-threaded JavaScript process. For a large codebase with thousands of files, this takes minutes.
Then in 2020, esbuild appeared. Written in Go. Same job, 10-100x faster. Then SWC appeared, written in Rust. Same speed gains. The JavaScript build tool landscape changed overnight.
Think of it like hand-washing dishes vs. a commercial dishwasher. The JavaScript-based tools (Babel, webpack's JavaScript core) are hand-washing — one dish at a time, using interpreted JavaScript, with garbage collection pauses and single-threaded execution. The native tools (esbuild in Go, SWC in Rust) are industrial dishwashers — compiled to native code, leveraging all CPU cores simultaneously, with manual memory management that eliminates GC pauses. Same dishes, same result, fundamentally different throughput.
Why Native Languages Are Faster
Before diving into specific tools, let's understand why Go and Rust are so much faster for build tools:
1. Compiled vs. Interpreted
JavaScript runs in V8, which JIT-compiles hot paths but still interprets cold code. Go and Rust compile directly to machine code — every function runs at native speed from the first call.
2. Parallelism
JavaScript is fundamentally single-threaded. Worker threads exist but have high overhead for message passing (structured cloning). Go has goroutines (lightweight threads with shared memory). Rust has fearless concurrency with ownership guarantees. Both can saturate all CPU cores efficiently.
3. Memory Management
V8's garbage collector periodically pauses execution to reclaim memory. This causes unpredictable latency spikes during builds. Go has a low-latency GC optimized for throughput. Rust has no GC at all — memory is managed at compile time through the ownership system.
4. No AST Serialization Overhead
This is the subtle one. JavaScript build tools often pass ASTs between different plugins via JSON serialization — the AST gets serialized to JSON, passed to the next plugin, deserialized back into objects. This serialization/deserialization cycle is shockingly expensive. Native tools keep the AST in memory as native data structures, passing pointers instead of copies.
esbuild: The Go Speedster
esbuild, created by Evan Wallace (co-founder of Figma), is a JavaScript/CSS bundler and minifier written in Go.
What esbuild Does
- Bundling — resolves imports and produces bundled output (like webpack, but simpler)
- Minification — compresses JavaScript and CSS (replaces Terser/cssnano)
- JSX/TSX transformation — converts JSX and TypeScript syntax (replaces Babel for these)
- CSS bundling — resolves
@import, handles CSS modules
What esbuild Doesn't Do
- No type checking — strips TypeScript types but doesn't validate them
- No complex transforms — no decorator support (legacy), no custom Babel plugins
- Limited plugin API — intentionally simple, covers basic hooks but not webpack-level extensibility
- No HMR — it's a build tool, not a dev server (Vite wraps esbuild for this)
Architecture: Why It's Fast
esbuild's internal pipeline:
Scanner (parallel)
→ Reads all source files concurrently using goroutines
→ Each file gets its own goroutine for I/O
Parser (parallel)
→ Parses each file into AST concurrently
→ Go's goroutines: lightweight, ~2KB stack each
Linker (parallel where possible)
→ Resolves imports, builds module graph
→ Determines chunk boundaries
Code Generator (parallel)
→ Generates output code from ASTs concurrently
→ Minification happens inline during generation
The key: everything that can be parallel, is parallel. esbuild doesn't process files sequentially. It spawns goroutines for reading, parsing, and generating code simultaneously. On an 8-core machine, it processes roughly 8 files at once throughout the pipeline.
Another trick: esbuild combines parsing and code generation into fewer passes. Traditional tools parse into a full AST, then walk the AST for transforms, then walk again for code generation. esbuild does as much as possible in a single pass.
# Benchmark: bundling a Three.js project (thousands of modules)
# Numbers from esbuild's published benchmarks
esbuild 0.33s
Parcel 2 10.3s
Rollup + terser 26.1s
webpack 5 33.4s
SWC: The Rust Transformer
SWC (Speedy Web Compiler), created by Donny (kdy1), is a Rust-based platform for JavaScript/TypeScript compilation. While esbuild is a bundler that also transforms, SWC is primarily a transformer that can also bundle.
SWC as a Babel Replacement
SWC is a drop-in replacement for Babel in most cases. It handles:
- TypeScript compilation (strips types, like esbuild)
- JSX transformation
- ECMAScript feature downleveling (like
@babel/preset-env) - Decorators (both legacy and TC39 proposal)
- React Fast Refresh integration
- Emotion/styled-components compile-time transforms
// .swcrc
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true
},
"transform": {
"react": {
"runtime": "automatic"
}
},
"target": "es2020"
}
}
Where SWC Is Used
SWC isn't just a standalone tool — it's embedded in major frameworks:
- Next.js — uses SWC as its default compiler (replaced Babel in Next.js 12+)
- Parcel 2 — uses SWC for JavaScript transformation
- Deno — uses SWC for TypeScript compilation
- Turbopack — built on SWC's parser and transformer
When you run next build, SWC is what compiles your TypeScript and JSX. You're already using it.
SWC Plugin System
Unlike esbuild's intentionally minimal plugin API, SWC supports WASM-based plugins that can perform custom AST transforms:
// A simplified SWC plugin in Rust
use swc_core::ecma::ast::*;
use swc_core::ecma::visit::{VisitMut, VisitMutWith};
struct MyTransform;
impl VisitMut for MyTransform {
fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
// Transform specific function calls
call.visit_mut_children_with(self);
}
}
SWC plugins compile to WASM, so they run at near-native speed within the SWC pipeline. This is a significant advantage over Babel plugins (which are JavaScript functions calling JavaScript APIs).
esbuild vs SWC: When to Use Each
| Capability | esbuild | SWC |
|---|---|---|
| Language | Go | Rust |
| Primary role | Bundler + minifier + transformer | Transformer + compiler (bundling via swcpack) |
| TypeScript | Strips types (no checking) | Strips types (no checking) |
| JSX | Yes | Yes |
| Decorators | No (legacy decorators not supported) | Yes (both legacy and TC39) |
| Preset-env equivalent | Limited (target option) | Yes (env configuration) |
| Custom plugins | Limited hooks | WASM-based AST visitors (like Babel) |
| Minification | Built-in, very fast | Via swc-minify (used by Next.js) |
| Used by | Vite (dev pre-bundling), Snowpack, tsup | Next.js, Parcel 2, Deno, Turbopack |
| CSS handling | Basic bundling + minification | Not a focus |
| Bundling | Full bundler | Experimental (swcpack) |
Use esbuild when:
- You need a fast bundler for dev tooling (Vite uses it for pre-bundling)
- You want fast minification (replace Terser with esbuild)
- You're building simple tools/scripts that don't need Babel's full transform capability
- You're using
tsupor similar tools that wrap esbuild for library publishing
Use SWC when:
- You're using Next.js (it's the default, no choice needed)
- You need decorator support
- You need Babel-level transforms with native speed
- You need custom AST plugins (WASM plugins are faster than Babel's JS plugins)
Limitations to Know
Both tools have important limitations compared to Babel:
esbuild Limitations
- No type checking — use
tsc --noEmitseparately - No legacy decorator support — only TC39 stage 3 decorators
- Minimal plugin API — can't do arbitrary AST transforms
- No polyfill injection — doesn't replace
@babel/preset-env+core-jsfor polyfills - Single-target output — no differential serving (modern + legacy bundles) in one build
SWC Limitations
- No type checking — same as esbuild, use
tscseparately - Plugin ecosystem is young — far fewer plugins than Babel's 10+ year ecosystem
- WASM plugin overhead — crossing the WASM boundary has some overhead for very simple transforms
- Bundler is experimental —
swcpackexists but is not production-ready; use Rollup/webpack for bundling
Neither esbuild nor SWC inject polyfills. If your code uses Array.prototype.at() and you target older browsers, the code will compile without errors but fail at runtime. Babel with @babel/preset-env and core-js is still the only tool that automatically detects and injects polyfills based on your browser targets. If you switch from Babel to esbuild/SWC, you need a separate polyfill strategy.
- 1esbuild and SWC are 10-100x faster than Babel/Terser because of native compilation, true parallelism, no GC pauses, and zero serialization overhead.
- 2esbuild is a bundler + minifier + transformer. SWC is primarily a transformer/compiler. Different tools for overlapping but distinct use cases.
- 3Neither tool type-checks TypeScript — both strip types only. Always run tsc --noEmit separately.
- 4Neither tool injects polyfills. If you need polyfills for older browsers, you need a separate strategy (core-js, polyfill.io, or manual).
- 5SWC supports decorators and WASM-based custom plugins, making it a more complete Babel replacement. esbuild's plugin API is intentionally minimal.
- 6If you use Next.js, you're already using SWC. If you use Vite, you're already using esbuild (for dependency pre-bundling).