Three words, three jobs

Minification wants your file smaller and your program unchanged. Uglification wants the same thing and is willing to rename the public names as well. Obfuscation wants your code hard to read and hard to analyse, and it pays for that with size and speed. The first two are optimisations. The third is a deliberate pessimisation.

MinifyUglifyObfuscate
Goalfewer bytesfewer bytes, names includedharder to read
Output sizesmallerslightly smaller stillmuch bigger
Runtime speedunchangedunchangedslower, measurably
Breaks external callersnoyes, if they use your globalsyes, in more ways
Formatting recoverableone clickone clickone click, and still unreadable

People mix them up because for fifteen years one npm package did the first two and the marketing pages of the third borrowed the vocabulary of the first. Worth untangling, because the wrong choice either breaks your build or slows your app down for no benefit.

From JSMin to Terser to esbuild

The whole category starts with Douglas Crockford’s JSMin, written in C and published in December 2003. It did one thing: remove comments and unnecessary whitespace. No parser, no scope analysis, no renaming. That was already enough to matter on a 56k modem, and it set the expectation that a minifier must never change behaviour.

Yahoo’s YUI Compressor followed in 2007, written in Java on top of the Rhino parser, and it was the first widely used tool that actually understood JavaScript well enough to rename local variables safely. Then Google open-sourced the Closure Compiler in November 2009, which is a different animal: a whole-program optimiser that inlines functions, removes unreachable code across file boundaries and, in ADVANCED mode, renames properties too. It produces the smallest output of anything in this list and it will happily break code that was not written with it in mind, which is why almost nobody outside Google runs ADVANCED mode on third-party libraries.

UglifyJS arrived around 2010, by Mihai Bazon, and it won on ergonomics: the first good minifier written in JavaScript itself, running on Node, dropping straight into Grunt, Gulp and later webpack. That is the entire reason “uglify” became a verb. The ES6 story got messy, though. The uglify-es branch that handled modern syntax stopped being maintained, and in 2018 it was forked into Terser, which picked up the ES6+ work and became the default minifier in webpack 4 and up, Rollup, Angular and Next.js. If a build step in your project minifies JavaScript today, it is probably Terser.

The newcomers are esbuild (Go) and SWC (Rust), and their pitch is speed. Numbers from the privatenumber/minification-benchmarks suite on the 2.13 MB victory bundle, which is the comparison we keep coming back to:

MinifierTimeOutput, gzipped
Terser3,359 ms158.46 KB
uglify-js1,105 ms167.58 KB
SWC244 ms157.73 KB
esbuild135 ms181.23 KB

So esbuild is roughly 25 times faster than Terser here and pays about 23 KB gzipped for it, while SWC is around 14 times faster at a gzipped size level with Terser’s. Our reading: SWC is the better default for a large app’s CI, Terser still wins when you are shipping a library and every kilobyte is somebody else’s bundle. Both of our JavaScript tools run Terser itself, bundled into the page and executed in your tab, so the JS minifier gives you the same output your build pipeline would, live as you type. Nothing you paste leaves the browser, which is the only reason we would use a web tool on proprietary source at all.

What minification actually removes

Four categories, in roughly the order of how much they save:

  • Whitespace and comments. Indentation, line breaks, JSDoc blocks. The JSMin job, still the most visible change.
  • Name mangling. Local variables and function parameters get renamed to a, b, c. Only names that cannot be observed from outside, which is the safety rule the whole thing rests on.
  • Dead code elimination. Constant folding, branches that can never run, unreachable statements after a return, functions nothing calls. Terser will also drop your leftover console.log calls when you ask it to.
  • Syntax squeezing. true becomes !0, consecutive declarations merge, if/else turns into a ternary or a comma expression. This is where minified code gets its characteristic look.

One thing minification deliberately keeps: licence banners. MIT, BSD and Apache all require the notice to travel with the code, so minifiers recognise the /*! … */ convention and preserve those blocks. Our minifier exposes that as the --keep-license flag, off by default because most people are minifying their own application code, on with one click when you are bundling a dependency.

And one thing no minifier touches: string literals. Every URL, every endpoint path, every hardcoded token in your source comes out the other side character for character. That matters more than it sounds, and it leads directly to the next section.

Why minified code isn’t protected

“We minify it so people can’t steal it” is the single most common misunderstanding here, and it survives because minified code looks impenetrable. It isn’t. Open the Sources panel in Chrome DevTools, hit the pretty-print button in the bottom left, and the bundle comes back fully indented. Firefox has the same feature. The formatting is not information, it is presentation, and it is regenerated from the syntax tree in milliseconds.

What is genuinely gone after minification is the identifier names and the comments. Everything else is intact: the control flow, the module boundaries, the API calls, the strings. Anyone reading a pretty-printed bundle can follow what it does, it just takes longer without meaningful names.

The dangerous version of this belief is the one that ends with credentials in a bundle. A minified API key is an API key in plain sight, and so is a base64-encoded one, which is a mistake we see often enough that it has its own article. If a value must stay secret, it has to live on a server your users don’t control. There is no client-side transformation that changes that.

What “uglify” means today

Now that the tool named UglifyJS is no longer the one everybody runs, the word has settled into meaning one specific thing: mangle the top-level names too.

Default minification leaves top-level identifiers alone, because in a classic script tag they are globals, and something else on the page might call them. Turn that restriction off (Terser’s toplevel option, historically --toplevel) and every function and variable in the file gets a one-letter name, which typically buys you a few more percent. Our JS uglifier is exactly that: the same Terser engine, same live output as you type, with top-level mangling switched on.

The failure modes are predictable once you know where the boundary is. Top-level mangling breaks anything that reaches your code by name from outside the file: an onclick attribute in your HTML calling a global function, a second script tag on the page, a test that pokes at internals, or any string-based dependency injection like the classic AngularJS annotation style.

The subtler one people lose an afternoon to: mangling changes Function.prototype.name and class names. Code that reads obj.constructor.name to decide what to do, or a DI container that resolves dependencies from constructor names, works perfectly in development and returns t in production. Terser has keep_fnames and keep_classnames for that case, and reaching for them is a legitimate fix, not a hack.

Real obfuscation, and what it costs

Obfuscation is a different intent. The reference implementation in the JavaScript world is javascript-obfuscator, and its transformations are worth knowing even if you never use it, because you will eventually have to read code that went through it.

  • String array. Every string literal is moved into one array and replaced by a call to a decoder function, optionally with base64 or RC4 encoding and a rotation so the array order looks arbitrary. This is the transformation that hides the strings minification leaves in the clear.
  • Control flow flattening. Sequential code is rewritten into a dispatcher: a while loop around a switch statement, driven by an index array, so the original order of operations is no longer visible in the source order.
  • Dead code injection. Plausible-looking code that never runs, inserted to waste a reader’s attention.
  • Self defending and debug protection. Code that breaks if reformatted, plus debugger traps that make stepping through it painful.

The price is documented by the project itself, which we appreciate. The README says control flow flattening “greatly affects the performance up to 1.5x slower runtime speed”, that dead code injection increases the size of the obfuscated code by up to 200%, and, in the general warning about obfuscating vendor scripts, that obfuscated code runs 15% to 80% slower depending on which options you enable. Read that next to the minifier table above: you spend an hour tuning a bundler to save 23 KB, then hand back multiples of that plus a chunk of runtime speed.

Obfuscation is defensible when there is a contractual or licensing reason, for example code you ship to a customer’s site that you are required to protect, or a game client where casual cheating is a real cost. It is not defensible as a substitute for server-side authorisation, and it does not stop anyone determined. Automated deobfuscators handle the standard string-array-plus-flattening combination routinely, because the dispatcher pattern is trivially recognisable.

Minify vs gzip: the honest numbers

Here is the part most comparison articles skip. Minification typically cuts raw source dramatically, often close to half for hand-written code, and that is the number every tool puts on its landing page. But nobody transfers raw source. Your server sends the file with Content-Encoding: gzip or br, and compression is extremely good at exactly the things minification removes first. Long repeated identifier names and runs of indentation are textbook redundancy; a compressor eats them nearly for free.

What survives compression is the part of minification that removes information rather than redundancy: the dead branch that is now gone, the unused function that no longer exists, the identifier that shrank from eleven characters to one and therefore changed the entropy of the file. That is why two minifiers can produce raw outputs 15% apart and gzipped outputs 2% apart, and why the gzip column in the benchmark table above is the one that decides anything.

It is also why our minifier prints the gzipped size of the output next to the original and the minified size. Optimising against the raw number leads you to conclusions that never show up in a waterfall chart. And while you are counting bytes: on a typical content site the JavaScript is not the biggest thing on the page, the images are, so the same afternoon spent on choosing between AVIF and WebP usually pays better. Getting the caching headers right pays best of all, because a hashed bundle filename with a one-year immutable cache means returning visitors download zero bytes of it.

Source maps and readable stack traces

Minified code produces stack traces like at t (main.4f2a1c.js:1:48213), which is useless on its own. Source maps fix that. The format is Source Map Revision 3, the JSON spec Google and Mozilla settled on back in 2011, with a mappings field of base64 VLQ segments translating generated positions back to original file, line and column. Every minifier and bundler in this article can emit one.

Two practical rules we stick to. First, a map is only useful if it matches the exact build it came from, so the map has to carry the same content hash as the bundle; a stale map produces stack traces that point at the wrong line, which is worse than no map because you trust it. Second, decide deliberately whether the map is public. A map generated with sourcesContent embeds your original, unminified source text, so publishing it publishes the project. Error trackers like Sentry take maps through their own upload API and resolve traces server-side, which gets you readable production errors without shipping your source to visitors.

If you only want to read a minified file once, you do not need a map at all. Open it in the Sources panel, hit pretty-print, and read that. The names stay short, but for a quick “what does this third-party script do on my page” question that is usually enough.

Minification, and the source-protection myth

What is the difference between minify and uglify?

Minifying strips whitespace, comments and dead code and shortens names inside functions, while leaving top-level names alone so other scripts can still call them. Uglifying does all of that and renames the top-level names too. The distinction is historical: UglifyJS was simply the tool everyone used from 2010 onwards, so its name turned into a verb. In today’s tooling both are the same engine with a different flag, in Terser’s case the toplevel option.

Does minifying JavaScript protect my source code?

No. Minification is a size optimisation, not a security measure. Any browser can restore the formatting: the Sources panel in Chrome DevTools has a pretty-print button that reindents a minified bundle in one click, and Firefox has the same thing. Variable names are gone, but the program structure is intact, and string literals such as URLs, endpoint paths and API keys survive minification completely untouched, because no minifier rewrites string contents.

Should I use Terser or esbuild for minification?

Terser if the last few kilobytes matter, esbuild if build time matters more. In the privatenumber/minification-benchmarks suite, minifying the 2.13 MB victory bundle takes Terser about 3,359 ms and esbuild about 135 ms, roughly 25 times faster, and esbuild’s output comes out around 181 KB gzipped against Terser’s 158 KB. SWC sits in between and is the interesting compromise: about 244 ms on the same artifact with a gzipped size essentially level with Terser.

How much smaller does minification make a JavaScript file?

Cutting raw source roughly in half is normal for hand-written code, but that number is misleading because your server sends the file compressed. Whitespace and long identifiers are exactly the kind of redundancy gzip and brotli already handle well, so the transfer-size saving is much smaller than the raw-byte saving. Judge a minifier by the gzipped size of its output, which is why our JS minifier shows that figure next to the raw one.

Is JavaScript obfuscation worth it?

Only when there is a business reason such as licence enforcement or anti-tampering, and never as a way to hide secrets. Obfuscation makes your bundle bigger and slower: the javascript-obfuscator README states that control flow flattening costs up to 1.5x slower runtime speed, that dead code injection can grow the output by up to 200%, and that obfuscated code generally runs 15% to 80% slower depending on the options. Anything that must stay secret belongs on the server.

Can minified JavaScript be reversed?

The formatting can be restored perfectly, the names cannot. A pretty-printer or any code formatter gives you back readable indentation and line breaks instantly, and the control flow, the strings and the call structure are all still there. What is lost for good is the original identifier names and the comments. A source map changes that: with the matching .map file the original file names, line numbers and even the original source text come back.

Do I still need to minify if my server uses gzip or brotli?

Yes, but for smaller gains than most people expect. Compression removes repetition, minification removes information: dead branches, unreachable code, whole unused functions and long identifier names disappear and never reach the compressor at all. The combination beats either one alone. What you should stop doing is quoting the raw-byte saving as if it were the number your users feel.

Should I upload source maps to production?

Upload them to your error tracker, not to your public web server, if the source is proprietary. A .map file with sourcesContent contains your original code verbatim, so anyone who requests it gets the unminified project. Sentry, Rollbar and friends accept maps through their own upload API and resolve stack traces server-side. If your code is open anyway, serving maps publicly is fine and makes debugging in the wild much easier.