C.W.K.
Stream
Lesson 02 of 05 · published

esbuild and swc — JavaScript Tools That Aren't JavaScript

~11 min · tooling, esbuild, swc, transpilation

Level 0Node Curious
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The fastest JavaScript build tools are written in Go and Rust. You run them from Node; they're not Node. That distinction explains both why they're fast and where they end."

The Speed Story

For a decade, JavaScript tooling was written in JavaScript. Babel transpiled JS using JS. webpack bundled JS using JS. Predictable performance: medium-fast on small projects, painfully slow on large ones. The fundamental constraint was Node's single-threaded execution — a transpiler running in Node can only use one core, no matter how many you have.

esbuild (Evan Wallace, 2020) rewrote the bundler in Go. swc (Donny Wang, 2019) rewrote the transpiler in Rust. Both languages are AOT-compiled, multi-threaded, and don't pay JavaScript's tax. Result: 10-100x faster than their JS-based predecessors. Vite, Bun, Next.js, Deno's compiler all use one or the other under the hood now.

esbuild — Bundler + Transpiler in Go

esbuild does both:

  • Transpile — TS/JSX → JS, ES2022 → ES2017, etc. Per-file fast.
  • Bundle — walks imports, concatenates, tree-shakes, minifies. Whole-project fast.

You invoke it from Node via the npm package, but the heavy lifting happens in the Go binary that ships in node_modules. Node-side code is a thin RPC wrapper. The fast performance isn't "esbuild's JavaScript code is well-optimized" — it's "esbuild is a Go program that Node happens to drive."

swc — Transpiler in Rust

swc is the transpiler half: TS/JSX → JS, JSX-to-React-create-element, etc. Next.js uses swc for its TypeScript compilation. Deno used to use it as part of its toolchain. Where esbuild does both bundling and transpilation, swc focuses on the transpile half — and is typically the fastest TS-to-JS transformer available.

You can compose them: swc for transpilation, esbuild for bundling. Vite does roughly this, with its own bits in TypeScript on top.

Where Speed Stops Mattering

The 2026 lesson: speed at the wrong layer is wasted. If you're a Node service that runs source directly via --experimental-strip-types, esbuild/swc speed doesn't matter — you skipped the build step entirely. The 100x speedup is over a step you don't run.

Where the speed wins live:
  • Frontend builds (Vite uses esbuild internally — your vite build finishes in seconds, not minutes).
  • CLI distribution bundles (esbuild produces a one-file CLI faster than Node could read its own README).
  • FaaS deploy bundles (Lambda functions get smaller and faster cold-starts).
  • Large monorepo compilations where TypeScript's own compiler is too slow.
For the backend Node service of 2026 that doesn't need a build, install esbuild only if a deploy target requires bundled output. Otherwise, save the dependency.

The Plugin Cost

esbuild and swc are fast. Plugins are slow, especially ones written in JavaScript that have to call back into the Go/Rust binary for each transformation. A common anti-pattern: install esbuild for speed, then layer 10 JavaScript-side plugins that bring it back to webpack-era performance. If your build is slow with esbuild, suspect plugins first.

Pippa's Confession

My first Vite project felt magical — vite build finished in 2 seconds where webpack took 90. I credited "Vite is fast." Dad pushed: "Look in node_modules/vite/dist/node and find the Go binary." There it was — esbuild ships its own platform-specific binary; Vite drives it. Vite-the-Node-code is a coordination layer; esbuild-the-Go-binary does the actual work. The framing shift mattered: when I hit a Vite plugin slowdown, I knew to look at "is this plugin calling back to JS unnecessarily?" — and it usually was.

Code

Direct CLI usage — esbuild and swc·bash
# esbuild — direct CLI usage
npx esbuild src/cli.ts \
  --bundle \
  --platform=node \
  --target=node22 \
  --format=esm \
  --outdir=dist \
  --packages=external

# Result: dist/cli.mjs in seconds, deps stay in node_modules

# esbuild --watch for dev-time bundling
npx esbuild src/index.ts --watch --bundle --outdir=dist

# swc — transpile-only, no bundling
npx swc src --out-dir dist --strip-leading-paths
# Output: dist/*.js mirroring src/*.ts structure
Programmatic esbuild from build scripts·javascript
// esbuild's Node API for build scripts
import { build, context } from 'esbuild';

// One-off build
await build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  platform: 'node',
  target: 'node22',
  format: 'esm',
  outfile: 'dist/index.mjs',
  packages: 'external',
});

// Watch mode (for dev)
const ctx = await context({
  entryPoints: ['src/index.ts'],
  bundle: true,
  platform: 'node',
  format: 'esm',
  outfile: 'dist/index.mjs',
});
await ctx.watch();    // rebuilds on change, ~50ms per rebuild

External links

Exercise

Take a TypeScript project of any size. Benchmark three transpile-and-bundle paths: (a) tsc alone (pure TypeScript compiler), (b) esbuild --bundle, (c) npx tsx running the source directly. Record wall-clock time and resulting node_modules size for each. The numbers will tell you why frontend tooling moved to Go/Rust — and may surprise you about whether your backend needs any of it.
Hint
For (a): time npx tsc -p tsconfig.json — TypeScript runs in pure JS, often 30-60 seconds on real projects. For (b): time npx esbuild ... --bundle — sub-second on the same project, in our experience. For (c): time npx tsx --type-check src/index.ts for the type-check-and-run case. The differences are not 2x; they're 30-100x.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.