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 injection —
core-jsfor 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-envwith 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_modulesonce 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-swcor similar, SWC handles the transform. Otherwise, esbuild can do prod transforms too. - Babel: only if you opt in via
@vitejs/plugin-reactwith babel options orvite-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 --noEmitin CI or pre-commit.- IDE (
tsserver) during development. - Builds use SWC/esbuild/Babel for transform;
tscruns 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 onbrowserslist. - SWC +
@swc/helpers+core-js— similar, configured in.swcrc. - esbuild — none built-in. You configure
browserslistand 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-swcfor 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 likeemitDecoratorMetadata— SWC supports via plugin; esbuild doesn’t.isolatedModules: truein tsconfig is required when transforming files individually (which is what all three do). Forces you to write code that’s per-file-compilable (noconst 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 --noEmitseparately in CI.” - “Vite uses esbuild for transforms by default; SWC via
@vitejs/plugin-react-swcis a common upgrade for React projects.” - “Next.js dropped Babel for SWC in v12; a
.babelrcin 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
- Vite + bundler choice: 01_vite_vs_webpack_vs_turbopack.md
- TypeScript strictness (the type checker side): ../04_typescript/09_strictness_flags.md
- Bundle size + polyfill audit: ../15_performance/03_bundle_analysis_and_code_splitting.md
Further reading
- esbuild docs: https://esbuild.github.io/
- SWC docs: https://swc.rs/
- Babel docs: https://babeljs.io/
- Next.js — SWC compiler: https://nextjs.org/docs/architecture/nextjs-compiler