Understanding JS minification
Rename, fold, drop.
What Terser and esbuild do beyond whitespace — variable renaming, dead-code elimination, constant folding — and the source-map workflow that makes debugging tolerable.
Three transformation tiers.
Tier 1 — whitespace and comments. Same as CSS. Pure mechanical stripping. Tier 2 — identifier renaming. Local variables become single letters (a, b, c) since their names don't matter outside the function. Tier 3 — semantic transformations: dead code elimination (if (false) {...} drops), constant folding ("2 + 3" becomes "5"), inline functions called once, tree-shaking exports nothing imports.
Tree-shaking — the modern win.
If a module exports 50 functions and your app imports 3, tree-shaking removes the other 47 from the bundle. Requires ES modules (import/export syntax — CommonJS doesn't tree-shake reliably). Lodash-es shaves bundles dramatically over CommonJS lodash; date-fns vs Moment.js is a famous case (date-fns is tree-shakeable, Moment isn't). For library authors, ship ES modules; for app authors, prefer tree-shakeable libraries.
A worked minify.
Input: function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price;
}
return total;
} Output: function calculateTotal(t){let e=0;for(let n=0;n<t.length;n++)e+=t[n].price;return e}. From 130 bytes to ~85 bytes raw. Variable renaming saves bytes; tree-shaking (if the function is unused) saves everything.
Rename + whitespace
locals → 1-char names ; whitespace gone
Public API (calculateTotal) preserved; internals shrink.
130 bytes → 85 bytes
= ~35 % raw reduction
Terser vs esbuild vs SWC.
Terser (the long-time default) is written in JS, slow, very thorough. esbuild (Go) and SWC (Rust) are 10-100× faster, slightly less aggressive on some optimisations. For most projects the speed wins (a 200-file frontend minifies in seconds with esbuild, minutes with Terser). The byte-count difference between them is single-digit percent. Modern toolchains (Vite, Next.js, Astro) use SWC or esbuild by default.
Source maps are mandatory.
Minified JS in production stack traces is unreadable. Source maps map the minified line/column back to the original source. Most error-tracking services (Sentry, Bugsnag, Rollbar) require source maps to be uploaded to translate stack traces. Ship the source maps to your error tracker; don't serve them publicly (they leak source structure). The build pipeline: minify with source map → upload map to Sentry → serve only the minified file.
The "minified prod, source dev" workflow.
Don't minify during development — the source-map indirection slows the edit-refresh loop, errors are harder to read in real time, and dev builds should be as close to "what you wrote" as possible. Minify only in production builds. Vite, Webpack, Parcel all do this automatically based on the build mode (NODE_ENV). Override only if you're debugging a minification-specific issue.