frontend / build tools / 02_esbuild_vs_swc_vs_babel.md

esbuild vs SWC vs Babel

5 min read source

esbuild vs SWC vs Babel

TL;DR

Three TypeScript/JSX transformers. Babel is the long-time standard — JS-implemented, huge plugin ecosystem, slow. SWC (Speedy Web Compiler) is Rust-native, ~20× faster, used by Next.js + Parcel. esbuild is Go-native, ~50× faster than Babel, used by Vite for dev + tools like tsx. Most apps in 2026 can use SWC or esbuild and only fall back to Babel for specific plugin needs. SWC is the more general-purpose answer; esbuild is the bare-metal speed answer.

Interview Q&A

Q: What does a transformer actually do?

A: Takes source code (TS, JSX, modern JS) and produces output JS the browser/runtime can execute. Includes:

  • Type stripping — remove TypeScript type annotations (types don’t affect runtime).
  • JSX → JS<Button>React.createElement(Button) or _jsx(...).
  • Syntax downleveling — async/await → state machine for older targets.
  • Polyfill injectioncore-js for missing built-ins (when configured).
  • Optional optimizations — minification, dead-code elimination.

This is separate from the bundler (Vite, Webpack), though most bundlers invoke a transformer per file.

Q: Why is Babel slow?

A: Babel is JS — it parses with its own JS parser, builds an AST, runs JS-implemented plugins that traverse and modify the AST, prints the result. Every step is JS.

For a 100K-line codebase: Babel takes 30+ seconds; SWC takes 1-2; esbuild takes < 1.

Rust (SWC) and Go (esbuild) are 10-50× faster at the same work, and they parallelize across CPU cores naturally.

Q: SWC vs esbuild — feature coverage.

A:

SWC esbuild
TS / JSX yes yes
Syntax downleveling yes (configurable targets) limited (mostly modern target, fewer transforms)
Polyfill injection yes (via swc/plugin-transform-imports etc.) no — bring your own
Plugin ecosystem growing minimal by design
Custom plugins Rust or WASM very limited (Go)
Used by Next.js, Parcel, Vitest (TS), Vite Rolldown Vite (dev pre-bundling), tsx, esbuild-loader

SWC is more general-purpose. esbuild is faster, narrower, designed to be embedded. Vite uses esbuild for fast dep pre-bundling in dev but Rollup (which can use SWC via a plugin or Babel via @rollup/plugin-babel) for production.

Q: When do you still need Babel?

A:

  • Plugins without SWC/esbuild equivalent. Niche transforms (some testing utilities, custom DSL plugins, etc.). Check first — common plugins (emotion, styled-components, relay) have ports.
  • @babel/preset-env with very specific browser targets. SWC supports targets but the resolution is less granular.
  • Building production polyfills with core-js — SWC supports it; Babel was the original.
  • Legacy projects with a Babel config that works.

For new projects: start without Babel. Add only if you hit a specific need.

Q: Vite uses both — explain.

A: Yes, Vite uses both transformers in different phases:

  • Dev pre-bundling: esbuild bundles node_modules once at startup (and on dep change). Fast cold start.
  • Dev TS/JSX transform: by default Vite uses esbuild for per-file TS/JSX transforms (no type checking, just strip + convert).
  • Production: Rollup. If you configure @rollup/plugin-swc or similar, SWC handles the transform. Otherwise, esbuild can do prod transforms too.
  • Babel: only if you opt in via @vitejs/plugin-react with babel options or vite-plugin-babel.

The default is esbuild for everything; SWC is an upgrade path for projects that need richer transforms.

Q: Next.js uses SWC — what’s the deal?

A: Next.js dropped Babel as the default in version 12 (2021), built SWC into the toolchain. By default, Next.js transforms TS/JSX with SWC. Custom Babel plugins (via .babelrc) still work but force SWC off, slowing builds significantly.

If you’re on Next.js and your build is slow, check for a .babelrc — removing it (and porting any plugins to SWC equivalents) can cut build times by 5-10×.

Q: TypeScript: do you need it for type checking?

A: No transformer does type checking — they strip types. For type checking:

  • tsc --noEmit in CI or pre-commit.
  • IDE (tsserver) during development.
  • Builds use SWC/esbuild/Babel for transform; tsc runs alongside for check.

Some projects skip CI type-checking to speed things up. Don’t — type errors will ship, but the build won’t catch them. Type-check in CI as a separate parallel job.

Q: Polyfills — who handles them?

A: A separate concern from transformation.

  • Babel + @babel/preset-env + core-js — auto-inject polyfills based on browserslist.
  • SWC + @swc/helpers + core-js — similar, configured in .swcrc.
  • esbuild — none built-in. You configure browserslist and explicitly import the polyfills you need, or use @rollup/plugin-polyfill-node-style plugins.

For modern-only targets (no IE 11), polyfills are mostly unnecessary — most browsers ship the features natively. Audit your browserslist: removing IE 11 alone often shaves 30-100 KB of polyfills.

Q: When SWC vs esbuild for a Vite project?

A: Vite defaults to esbuild for transforms. Switch to SWC when:

  • You need plugins (e.g., @vitejs/plugin-react-swc for SWC-based React Fast Refresh — faster than Babel-based default).
  • You need richer syntax transforms (downleveling to old targets, decorators with metadata, specific TC39 stage plugins).

@vitejs/plugin-react-swc is a common upgrade — keeps esbuild for everything else, uses SWC for the React transform.

Gotchas / edge cases

  • Decorators — TS, SWC, Babel all support them but with different syntax/spec stages. Lock your target spec and pick a transformer that supports it.
  • useDefineForClassFields — TS default changed; SWC honors it via .swcrc. Esbuild has its own setting. Mismatches cause subtle field-init bugs.
  • emit* TS options like emitDecoratorMetadata — SWC supports via plugin; esbuild doesn’t.
  • isolatedModules: true in tsconfig is required when transforming files individually (which is what all three do). Forces you to write code that’s per-file-compilable (no const enum, etc.).
  • JSX runtime — both classic (React.createElement) and automatic (_jsx) are supported; configure per transformer (jsx: "automatic" in tsconfig + transformer setting).
  • Babel macros are compile-time JS — no SWC/esbuild equivalent. Migrate or stay on Babel for those.

What a senior is expected to say

  • “Babel is JS-implemented and slow; SWC (Rust) and esbuild (Go) are 10-50× faster. Use SWC or esbuild by default; Babel only when a plugin requires it.”
  • “Transformers strip types; they don’t type-check. Run tsc --noEmit separately in CI.”
  • “Vite uses esbuild for transforms by default; SWC via @vitejs/plugin-react-swc is a common upgrade for React projects.”
  • “Next.js dropped Babel for SWC in v12; a .babelrc in a Next project disables SWC and tanks build speed. Remove unless absolutely needed.”
  • “Polyfills are a separate concern — pinned to browserslist. Drop legacy targets (IE 11) to shave KBs.”

Cross-references

Further reading