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-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.
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-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.
Vite uses both — explain.
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.
// 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:
{
"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 --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.
Polyfills — who handles them?
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.
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-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
- 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.
Related
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
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 --noEmithas 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
.babelrcorbabel.config.jsin 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
browserslistrather than by the transformer. Syntax transformation and runtime polyfills are different concerns, and dropping legacy targets is usually the cheapest available bundle reduction.