Skip to content

esbuild and SWC: Native Speed

intermediate17 min read

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.

Mental Model

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.

Quiz
What is the primary reason esbuild (Go) and SWC (Rust) are faster than Babel (JavaScript)?

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
Quiz
esbuild processes files using Go goroutines for parallelism. Why can't JavaScript build tools achieve the same parallelism with Worker Threads?

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

CapabilityesbuildSWC
LanguageGoRust
Primary roleBundler + minifier + transformerTransformer + compiler (bundling via swcpack)
TypeScriptStrips types (no checking)Strips types (no checking)
JSXYesYes
DecoratorsNo (legacy decorators not supported)Yes (both legacy and TC39)
Preset-env equivalentLimited (target option)Yes (env configuration)
Custom pluginsLimited hooksWASM-based AST visitors (like Babel)
MinificationBuilt-in, very fastVia swc-minify (used by Next.js)
Used byVite (dev pre-bundling), Snowpack, tsupNext.js, Parcel 2, Deno, Turbopack
CSS handlingBasic bundling + minificationNot a focus
BundlingFull bundlerExperimental (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 tsup or 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)
Quiz
Your project uses legacy decorators and several custom Babel plugins for compile-time transforms. You want to speed up builds. Which tool is the best replacement?

Limitations to Know

Both tools have important limitations compared to Babel:

esbuild Limitations

  • No type checking — use tsc --noEmit separately
  • 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-js for polyfills
  • Single-target output — no differential serving (modern + legacy bundles) in one build

SWC Limitations

  • No type checking — same as esbuild, use tsc separately
  • 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 experimentalswcpack exists but is not production-ready; use Rollup/webpack for bundling
Common Trap

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.

Key Rules
  1. 1esbuild and SWC are 10-100x faster than Babel/Terser because of native compilation, true parallelism, no GC pauses, and zero serialization overhead.
  2. 2esbuild is a bundler + minifier + transformer. SWC is primarily a transformer/compiler. Different tools for overlapping but distinct use cases.
  3. 3Neither tool type-checks TypeScript — both strip types only. Always run tsc --noEmit separately.
  4. 4Neither tool injects polyfills. If you need polyfills for older browsers, you need a separate strategy (core-js, polyfill.io, or manual).
  5. 5SWC supports decorators and WASM-based custom plugins, making it a more complete Babel replacement. esbuild's plugin API is intentionally minimal.
  6. 6If you use Next.js, you're already using SWC. If you use Vite, you're already using esbuild (for dependency pre-bundling).