Frontend / Build tools / 02_esbuild_vs_swc_vs_babel.md

esbuild vs SWC vs Babel

Updated 4 interview angles 6 min read source
On this page13
  1. What does a transformer actually do?
  2. Why is Babel slow?
  3. SWC vs esbuild — feature coverage.
  4. When do you still need Babel?
  5. Vite uses both — explain.
  6. Next.js uses SWC — what’s the deal?
  7. TypeScript: do you need it for type checking?
  8. Polyfills — who handles them?
  9. When SWC vs esbuild for a Vite project?
  10. Gotchas
  11. Related
  12. Further reading
  13. Interview angle

esbuild vs SWC vs Babel

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.

What does a transformer actually do?

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 injection — core-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.

Why is Babel slow?

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.

SWC vs esbuild — feature coverage.

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.

When do you still need Babel?

  • 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.

Vite uses both — explain.

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.

ts
// vite.config.ts — the swap is one plugin.
// not plugin-react
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
  plugins: [react()],
  // still used for deps
  esbuild: { target: 'es2022' },
})

And because none of these three type-check, the check is a separate process that must exist somewhere:

json
{
  "scripts": {
    "build": "tsc --noEmit && vite build",
    "dev": "vite"
  }
}

tsc --noEmit in the build script is the whole answer to “who checks types”. Leaving it out means a type error ships — the transformer stripped the annotations without ever reading them.

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

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×.

TypeScript: do you need it for type checking?

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.

Polyfills — who handles them?

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.

When SWC vs esbuild for a Vite project?

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

  • 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.

Further reading

Interview angle 4

  • “Why did the ecosystem move off Babel?” - speed. Babel is written in JavaScript; SWC is Rust and esbuild is Go, and both are an order of magnitude or more faster on the same transform. Use one of those by default and keep Babel only where a specific plugin has no equivalent.
  • “Do these type-check your TypeScript?” - no, and this is the important one. They strip types without understanding them, so a type error compiles cleanly and ships. tsc --noEmit has to run separately in CI or you have no type checking at all.
  • “A Next.js build got dramatically slower. Where do you look?” - a .babelrc or babel.config.js in the project. Next dropped Babel for SWC in v12, and the presence of a Babel config silently disables SWC and falls back to the slow path. Remove it unless a plugin genuinely requires it.
  • “Where do polyfills fit?” - separately, driven by browserslist rather than by the transformer. Syntax transformation and runtime polyfills are different concerns, and dropping legacy targets is usually the cheapest available bundle reduction.