DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

JavaScript Minification Deep Dive: From 200KB to 40KB Without Breaking Anything

The first time I ran Terser on a client's JavaScript bundle and watched it drop from 198KB to 43KB, I assumed something must be broken. Nothing was. The minifier had done what minifiers do: removed every byte that the JavaScript engine does not need. Understanding what those bytes are, and which removals are safe, is the difference between a fast site and a broken one.

What a JavaScript minifier does

The transformations happen in layers, each more aggressive than the last.

Whitespace and comment removal. This is the safest and most obvious step. JavaScript does not care about indentation, blank lines, or comments at runtime.

// Before (readable)
function calculateTotal(items) {
  // Sum all item prices
  let total = 0;
  for (const item of items) {
    total += item.price * item.quantity;
  }
  return total;
}

// After (minified)
function calculateTotal(n){let t=0;for(const o of n)t+=o.price*o.quantity;return t}
Enter fullscreen mode Exit fullscreen mode

Variable renaming (mangling). Local variable names are shortened to single characters. total becomes t, items becomes n. The minifier analyzes the scope tree to ensure renames are safe -- it never renames variables that could collide with outer scopes or global references.

Dead code elimination. Code that can never execute is removed.

// Before
if (false) {
  console.log("This never runs");
}

// After: entirely removed
Enter fullscreen mode Exit fullscreen mode

Constant folding. Expressions that can be evaluated at build time are replaced with their results.

// Before
const SECONDS_PER_DAY = 60 * 60 * 24;

// After
const a=86400;
Enter fullscreen mode Exit fullscreen mode

Property access shortening. Repeated property accesses are sometimes collapsed.

// Before
console.log("a");
console.log("b");
console.log("c");

// After (Terser may alias console.log)
var l=console.log;l("a");l("b");l("c");
Enter fullscreen mode Exit fullscreen mode

Terser vs esbuild vs SWC

Terser is the successor to UglifyJS and the most established JavaScript minifier. It is written in JavaScript, which makes it slower than native alternatives but gives it the deepest understanding of JavaScript semantics. It has the most configuration options and produces the smallest output in most benchmarks.

esbuild is written in Go and is dramatically faster -- often 10-100x faster than Terser. It handles minification, bundling, and transpilation. The trade-off is that its minification is slightly less aggressive. In my measurements, esbuild's output is typically 2-5% larger than Terser's.

SWC is written in Rust and sits between the two in speed and output size. It is used by Next.js and Parcel.

For most projects, the choice comes down to build time vs output size. If your build takes 30 seconds and switching from Terser to esbuild drops it to 2 seconds while adding 3KB to the bundle, that is usually a good trade.

The mangling traps

Variable mangling is where most minification bugs originate. The minifier does not understand your application's runtime behavior -- it only analyzes static code.

Dynamic property access. If you access properties by string name, the minifier cannot know to rename them.

// This breaks after mangling
class Logger {
  logInfo(msg) { /* ... */ }
  logError(msg) { /* ... */ }
}

const method = "logInfo";
logger[method]("hello"); // Works before minification
// After mangling: logger["logInfo"] but the method is now logger.a()
Enter fullscreen mode Exit fullscreen mode

The fix is to either avoid dynamic access or tell the minifier not to mangle specific names.

eval(). Code passed to eval() runs in its own parsing context. The minifier cannot analyze it, so any variable referenced inside an eval string must keep its original name. Most minifiers detect eval and disable mangling for the entire scope, which significantly reduces compression.

Function.name. Some frameworks rely on function.name or constructor.name for dependency injection or serialization. Mangling renames functions, breaking these patterns.

class UserService {}
console.log(UserService.name); // "UserService" before, "a" after
Enter fullscreen mode Exit fullscreen mode

Configure your minifier to preserve class names if your code depends on them:

// Terser configuration
{
  mangle: {
    keep_classnames: true,
    keep_fnames: true
  }
}
Enter fullscreen mode Exit fullscreen mode

Source maps are not optional

In production, you will get error reports with stack traces pointing to a.js:1:34829. Without a source map, this is useless. With a source map, your error tracking tool (Sentry, Datadog, Bugsnag) can show you the original file, function name, and line number.

// Terser CLI
terser input.js -o output.min.js --source-map "url='output.min.js.map'"

// Webpack (automatically handled in production mode)
module.exports = {
  devtool: "source-map", // or "hidden-source-map" to not expose the map URL
};
Enter fullscreen mode Exit fullscreen mode

Use hidden-source-map if you do not want the source map URL in the minified file. Upload the map to your error tracking service separately.

Tree shaking vs minification

These are complementary but different. Tree shaking (dead code elimination at the module level) removes exported functions and classes that are never imported. Minification removes dead code within the remaining modules. You need both.

Tree shaking requires ES modules (import/export). If your dependencies use CommonJS (require/module.exports), the bundler cannot tree-shake them. This is why library authors are moving to ESM -- it enables better dead code elimination downstream.

// This can be tree-shaken (ESM)
import { debounce } from "lodash-es"; // Only debounce is included

// This cannot (CommonJS)
const { debounce } = require("lodash"); // Entire library is included
Enter fullscreen mode Exit fullscreen mode

Quick minification without a build step

Not every project has a Webpack or Vite setup. For standalone scripts, one-off optimizations, or estimating how much a minifier will save, I use a JavaScript minifier at zovo.one/free-tools/js-minifier that handles whitespace removal, variable mangling, and dead code elimination.

Minification is the lowest-effort, highest-impact performance optimization for most JavaScript-heavy sites. Set it up once, generate source maps, test the output, and forget about it. The bytes you save are free performance for every user on every page load.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)