Optimizing Javascript with Minification and Mangling
It still amazes me how many startups and blogs I see serving javascript that hasn't been minified. If you don't use a mangler in your production build process, you're literally sending your IP across a series of tubes and into the laps of your competitors. In this article we're going to discuss what to use, to obfuscate and otherwise ready your code for production, while making it smaller in the meantime.
We're going to start where we left off in
the last article.
If you need to catch up:
git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout prod-js2
npm install
TLDR
We could use this article to quickly go over how to add UglifyJSPlugin
to your
webpack config. But what would that serve? What are we doing with Uglify? If
your purpose is to get a tight production bundle without learning how it
happens. Then here you go.
In your webpack.prod.js
add these 2 lines like we've done before with the
definition at the top of the file:
const UglifyJSPlugin = require("uglifyjs-webpack-plugin")
And the plugin with the rest of the plugins:
plugins: [...new UglifyJSPlugin()]
And of course add the package in your terminal:
npm install uglifyjs-webpack-plugin
And presto! Better bundles. But how? Well, there are a few parts to what a tool like Uglify is doing.
Concatenation
Historically, Javascript was delivered to the client in a production bundle, Webpack serves one of the purposes for that bundle. Concatenating the files, or joining them into one big file that's faster to download in an HTTP 1.1 world. With HTTP2, concatenation might be an anti-pattern as HTTP2 prefers multiple small files. We'll look at that later.
Minify & Mangling
Then we have Minify. Whereby we replace long variable names with shorter, one letter variables. This serves both the purpose of making the file size smaller, but also, obfuscating the purpose of the code from anyone looking trying to read the prod bundle. To illustrate, let's minify something via the command line.
Let's install the babel minifier:
npm install babel-minify
Copy the following code to the bottom of src/main.js
for illustrative
purposes.
const globalVar = true
const something = function(someArgument) {
const longVariableName = someArgument
const result = function(longVariableName) {
return longVariableName * longVariableName + globalVar
}
console.log(result)
}
Now in the terminal run:
minify src/main.js -d dist/
You'll see dist/main.js
appears and looks like this:
require("./main.css"),
require("./content.css"),
require("./images/link.jpg"),
require("react"),
console.log(`Environment is ${process.env.NODE_ENV}`)
const globalVar = !0,
something = function(a) {
console.log(function(a) {
return a * a + globalVar
})
}
A few things happened with the minify:
- You'll notice that global variables, stuff in the global scope like
globalVar
aren't mangled, but thelongVariableName
are turned intoa
. This is possible because they are in a function scope, so as long as the name is consistent, it shouldn't matter what the variable is named. This is called variable Mangling and is why Uglify is called that. It obfuscates the code's purpose, though that's no security and can be reverse engineered with enough time and patience. - Since the function
longVariableName
is assigned the value ofsomeArgument
minify creates a shorthand and just uses the argument variable, thereby eliminating code. - Notice that
true
has become!0
which is 2 characters shorter and means, not false (since0
evaluates tofalse
). Kinda cool, kinda weird. - All whitespace is removed and semi-colons are added so the Javascript parser can use it. This is where minify gets it's name, but it's worth noting the semi-colon addition, since most people don't want to have to manage semi-colons. And with Babel, you don't have to anymore.
To UglifyJS or Babel?
The art of getting the smallest file size for your particular use case means learning the options and experimenting with them. Babel-Minify works with Babel.
npm install babel-minify-webpack-plugin
At the top of webpack.prod.js
:
const MinifyPlugin = require("babel-minify-webpack-plugin")
In the plugins add:
plugins: [...new MinifyPlugin()]
When we run npm run build
we get a main-bundle.js
that's only 816 bytes
.
And when we switch back to UglifyJSPlugin, we see it beats it at 811 bytes
.
Not much, but we have very little JS in our project so far. We can keep both
plugins in the project and comment them out for now, to see the difference. But
the short of it is, Babel uses Babel to parse and transform while, Uglify uses
it's own parsing & mangling code seperate from Babel.
Additional Considerations
Javascript optimization is an expanding field and can turn into a bit of an art
form. In some cases you'll want to do a straight up find and replace of certain
code. For instance process.env.NODE_ENV
can be replaced at compile time with
the value (ie. "production"
), since it never changes. You can see that in
action in the code of the output dist/main-bundle.js
.
Lots more substitutions can happen, if we're using immutable variables that don't change and precompute them.
This isn't as simple as a text Find & Replace. Since Babel is turning the code into an AST so as to fully understand how it would be compiled, it's able to ensure it's not breaking the logic while making complex optimizations.
This is some of the cutting edge work Facebook is doing with Prepack. Pre-evaluating code and scope hoisting will be covered later.
In Sum
While optimizations in Javascript can get a bit hairy, I wanted to show you the parts of the puzzle that tools like Uglify and Google Closure Compiler are using to reduce code size. Variable Mangling, Whitespace removal and code replacement are a part of that puzzle.
Next Up
There is one final point I want to make about optimization and that's compression. Compression is another subject that can get weedy pretty quick and is a very important field in Computer Science. We'll stick to what's relevant, for our purposes to make our file sizes even smaller.