Make UglifyJS way faster by using it sooner

Adam Hooper
3 min readAug 1, 2017

Grunt, Gulp, Browserify, Webpack — they all make the same rookie mistake.

The task is “minifying”: transforming many small-but-verbose JavaScript files (coders maintain these) and into a few large-but-terse JavaScript files that do the same thing (browsers download these).

All the JavaScript bundlers I’ve seen do it wrong. Here’s what they do:

  1. Read the many small files
  2. Lump them into a few big files
  3. Minify the big files

There are two problems. First, the minify step gets slower and slower when you add more code. Second, the minify results can’t be cached.

Adding Code Compounds Minify Wait Times

Here’s how a minifier works:

  1. It parses a JavaScript file into an Abstract Syntax Tree, or AST. (The AST is the in-memory representation browsers will use when they eventually run the code.)
  2. It modifies the AST without changing logic. Basically: it renames long variables like myTemporaryVariable to t. (There are other techniques that can shorten code a tiny bit more.)
  3. It outputs the AST as tersely as it can. For instance: it prints emptiness instead of comments; it doesn’t print extra spaces; and it writes !0 instead of true to save two bytes.

Not too complicated, right? It reads the source code and then spits it back out.

But: if you feed the minifier double the JavaScript, it will take longer than double the time to minify it. With each new piece of code, the minifier gets slower at minifying all the other pieces of code — even when they’re unrelated.

We want the minifier to be at most twice as slow when you give it double the JavaScript. That doesn’t happen when all the files are concatenated before they’re passed to the minifier.

Breaking The Cache

What if you change one character in just one of your many small JavaScript files?

Think of a normal development loop: edit a file; refresh the browser window; edit a file; refresh the browser window….

If you change a file that compiles to JavaScript (CoffeeScript, ESLint, Babel, JSX … basically everything), your computer will need to recompile that one file. Does it need to recompile the rest?

With the pipeline described above, yes: you have to minify the entire package every time one file changes.

It’s slow.

So slow.

And here’s where I face-palm: to speed things up, it seems every developer disables the minifier during development. Webpack’s built-in minifier is suggested for “production” mode only. Browserify’s de-facto minify plugin suggests you only use it in production.

Everybody’s wrong. You should enable the minifier during development: it means you’ll be testing the same code end-users will run. If there’s a bug in the minifier, you’ll fix it instead of deploying it.

I won’t rant any further about why splitting “development” and “production” environments is evil. (It is.) But surely we can agree that when a step is both very important and very slow, it’s better to make it fast than to nix it.

The solution is simple:

Minify Sooner

Bundlers should do this:

  1. Read the many small files
  2. Minify them, one at a time
  3. Lump them into a few bigger files

With this reordering, the minifier won’t face enormous files any more. It’s faster to minify one small file at a time than it is to minify a single file with all contents concatenated.

I tested with Webpack: minifying typical code early was 23% faster and made the output only 0.7% larger.

There was a minuscule increase in the final JavaScript size: with this arrangement, the minifier doesn’t minify the “lump-files-together” code. We’re counting bytes here, not kilobytes. This tiny size increase can go away if bundlers like Webpack produce minimal lump-files-together code. (Nowadays, that Webpack-generated code includes comments and whitespace.)

During development, this pipeline makes code edits hundreds of times faster, because Webpack won’t re-minify files it already minified.

If you’re not convinced, consider another benefit: parallel processing. The pipeline I suggest runs lots of independent minify steps: they can be run at the same time. If you have a 4-core machine, minification can be nearly four times faster than it is today. (Someday, anyway. Today’s bundlers don’t take advantage of multiple processors yet.)

Today’s Solutions

My test bundle ran using uglify-loader in Webpack, on real code. In Overview, a JavaScript-heavy website, I sped JavaScript build by about 50% and removed “development” (un-minified) versions of JavaScript code. In Overview’s development mode, recompiling after changing one file costs 400ms: fast enough that I haven’t bothered optimizing.

Even today, there’s no good reason to disable minification in development mode.

--

--