JavaScriptTranspilers (Babel, SWC)

Transpilers & Build Tools

You write modern JavaScript; your users run browsers of wildly different ages. A transpiler bridges that gap by translating new syntax into older, more widely supported syntax. Around transpilers grew an entire ecosystem of build tools — Babel, TypeScript, Vite, esbuild, SWC, webpack — that also bundle, minify, and optimize your code for production.

This page explains why transpilation exists, how the major tools relate to each other, and why the modern trend is actually toward less transpilation.

Why transpile?

TC39 ships new JavaScript features every year, but users don't all update their browsers on schedule. Without help, this modern code crashes on older engines:

JS
// Modern source — optional chaining + nullish coalescing (ES2020)
const city = user?.address?.city ?? 'Unknown'

A transpiler rewrites it into equivalent code that even an old engine understands:

JS
// Transpiled output (roughly)
var _user$address
var city =
  (_user$address = user === null || user === void 0 ? void 0 : user.address) ===
    null || _user$address === void 0
    ? 'Unknown'
    : _user$address.city !== null && _user$address.city !== void 0
      ? _user$address.city
      : 'Unknown'
Transpiler vs compiler
A compiler translates between different levels (TypeScript to JavaScript, C to machine code). A transpiler is a source-to-source compiler between similar levels — modern JavaScript to older JavaScript. In practice the words are used loosely and interchangeably.

Transpilation lets you write for the future and ship for the present: developers use the latest language features immediately, while the build step guarantees compatibility with whatever browsers the project must support.

Babel: the original workhorse

Babel made ES6 usable years before browsers supported it, and it's still everywhere. Its architecture is a pipeline: parse code into an AST, run plugins that transform the AST, print JavaScript back out.

  • Plugins — each transforms one feature, e.g. @babel/plugin-transform-arrow-functions

  • Presets — curated bundles of plugins; @babel/preset-env is the one you actually use

  • @babel/preset-react — transforms JSX; @babel/preset-typescript — strips TS types

Bash
npm install --save-dev @babel/core @babel/cli @babel/preset-env
npx babel src --out-dir dist

JSON
// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": "3.36"
      }
    ]
  ]
}

The magic of preset-env is that it transpiles only what your targets need. You declare targets with Browserslist in package.json:

JSON
{
  "browserslist": [
    ">0.5%",
    "last 2 versions",
    "not dead",
    "not op_mini all"
  ]
}

If every targeted browser already supports arrow functions, Babel leaves them alone. Tighten your targets and your bundle shrinks; loosen them and Babel does more work. The same Browserslist config also drives Autoprefixer (CSS) and other tools.

Polyfills vs syntax transforms

Transpilers can only rewrite syntax. New APIsPromise, Array.prototype.at, structuredClone — are runtime features that can't be expressed in older syntax; they must be polyfilled: a script defines the missing feature at runtime.

Syntax transform

Polyfill

What it fixes

New grammar: arrow functions, classes, ?.

New APIs: Promise, fetch, Array.at

How

Rewrites code at build time

Defines the feature at runtime

Tool

Babel, SWC, esbuild, TypeScript

core-js, or targeted single polyfills

Cost

Slightly larger/uglier output

Extra runtime code shipped to everyone

The standard polyfill library is core-js. With useBuiltIns: "usage" (shown above), Babel automatically injects only the core-js polyfills your code actually uses, sized to your Browserslist targets.

A transform cannot save you from a missing API
If your code calls fetch on a runtime that lacks it, no amount of transpilation helps — the function simply does not exist. Know which of your problems are syntax problems (transpile) and which are API problems (polyfill).
TypeScript's compiler as a transpiler

The TypeScript compiler tsc does two independent jobs: type checking and emitting JavaScript. The emit side is a transpiler — it strips type annotations and can down-level syntax to a chosen target:

JSON
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",   // which JS version to emit
    "module": "ESNext",
    "outDir": "dist"
  }
}

In modern setups the two jobs are often split: a fast tool (esbuild, SWC, Babel) strips types and transpiles for speed, while tsc --noEmit runs separately just to check types. That's exactly how Vite handles TypeScript.

The modern toolchain: bundlers and fast transpilers

Transpilers rarely run alone — they're one stage inside a bundler, which resolves your import graph and produces optimized files for the browser:

Tool

Written in

Role

Known for

webpack

JavaScript

Bundler

Maximum configurability; the long-time default

Vite

JS (uses esbuild + Rollup)

Dev server + bundler

Instant dev startup, sensible defaults

esbuild

Go

Bundler + transpiler

Extreme speed (10–100x Babel)

SWC

Rust

Transpiler (+ bundling)

Babel-compatible speed; used by Next.js

Rollup

JavaScript

Bundler

Clean ES module output; powers Vite builds

Parcel

JS/Rust

Bundler

True zero-config experience

A typical Vite project shows the division of labor: during development, Vite serves your files as native ES modules and transpiles each file on demand with esbuild (milliseconds). For production, it bundles with Rollup, minifies, and applies your Browserslist targets. You get modern speed with legacy compatibility.

Bash
npm create vite@latest my-app
cd my-app && npm install
npm run dev      # instant dev server, on-demand transpilation
npm run build    # optimized production bundle in dist/
vite v5.4.2 building for production...
✓ 34 modules transformed.
dist/index.html                 0.46 kB │ gzip:  0.30 kB
dist/assets/index-BQx3Zq1k.js  143.21 kB │ gzip: 46.13 kB
✓ built in 1.02s
Source maps: debugging the code you wrote

Transpiled, bundled, minified output is unreadable — so how do you debug it? Source maps are companion .map files that map every position in the generated code back to your original source. With them, browser DevTools show your actual files, original variable names, and correct stack traces.

JS
// vite.config.js
export default {
  build: {
    sourcemap: true, // emit .map files alongside the bundle
  },
}
  • Generated files end with a comment like //# sourceMappingURL=index.js.map pointing at the map.

  • Error monitoring services (Sentry etc.) use uploaded source maps to un-minify production stack traces.

  • If you don't want to expose source publicly, upload maps to your error tracker but don't deploy them to the CDN.

Differential serving: module / nomodule

For a while, sites shipped two bundles: a small modern one for evergreen browsers and a heavier transpiled one for legacy browsers, using a clever HTML trick — old browsers ignore type="module", and modern browsers ignore nomodule:

HTML
<!-- Modern browsers load this (and skip the nomodule script) -->
<script type="module" src="/app.modern.js"></script>

<!-- Legacy browsers load this (and skip the module script) -->
<script nomodule src="/app.legacy.js"></script>

Today this pattern is mostly historical: browsers that don't support modules (essentially IE11) are gone from most audiences. If you still need it, @vitejs/plugin-legacy generates the legacy bundle automatically. Knowing that ES-module support itself is the compatibility line explains a lot of modern tooling defaults.

The trend: less transpilation

Here's the plot twist of the 2020s: the transpilation gap is closing. Evergreen, auto-updating browsers mean the features of ES2017–ES2020 (async/await, optional chaining, modules) are supported by effectively all users. The consequences:

  • Default build targets moved up — Vite targets roughly ES2020-era "baseline" browsers out of the box, not ES5.

  • Transpiling to ES5 by default now mostly wastes bytes: transpiled async/await (regenerator) is far larger and slower than the native syntax.

  • Node.js needs no transpilation at all for modern syntax — and since Node 22+ it can even strip TypeScript types natively for many files.

  • Speed replaced capability as the differentiator — hence Babel (JS) giving ground to esbuild (Go) and SWC (Rust).

  • Some libraries now publish untranspiled modern JavaScript and let the application's build pipeline decide how far down to compile.

Practical advice for new projects
Start with Vite defaults and a realistic Browserslist. Do not target ES5 unless analytics prove you have users who need it — every version you support below your real audience costs bundle size and runtime performance for everyone else.
Choosing your setup
  1. Simple site or learning project — no build step at all. Native ES modules in the browser work fine.

  2. Modern app (React, Vue, vanilla) — Vite. esbuild-powered dev server, Rollup production builds, TypeScript out of the box.

  3. Framework projects — Next.js (SWC), Astro/SvelteKit/Nuxt (Vite) bring a tuned pipeline; you rarely touch the transpiler directly.

  4. Legacy or highly custom builds — webpack + Babel remains the most configurable combination.

  5. Publishing a library — build with esbuild/Rollup/tsup, emit modern ESM (plus CJS if needed), and document your supported targets.

The mental model
Transpiler: rewrites new syntax into old. Polyfill: adds missing APIs at runtime. Bundler: stitches modules into optimized files. Source maps: keep it all debuggable. Browserslist: the single source of truth telling every tool how far back to reach.