Writing Javascript for Size
- Uglify and other compressors are way better now, and social norms for how large JavaScript can get have changed: people are much more willing to ship hundreds of kilobytes of code with a page.
- Algorithms based on tree-shaking, like Rollup, and pre-evaluation are now the bleeding edge.
Non-programmer readers, might want to skip this one, it’s pretty technical and relatively obscure.
my dinosaur, back when I had my Yashica
Anyway, back to the thing at hand. A while ago I read Will it Optimize? by the excellent ridiculous fish. It’s a fun adventure into what code GCC recognizes and optimizes, and what it doesn’t, or can’t.
This is a much simpler version of that, for Javascript, for size. I’ve been maintaining Wax , a mapping library that’s meant to be tiny, and slowly learning the tricks of the trade for writing tiny Javascript - not 140bytes-type evil tricks, but practical optimizations. Like how you get a library down to tiny sizes without making it a headache to maintain or making errors in the non-minified source untraceable.
Most of this comes down to understanding the magic of uglifyjs, the most interesting Javascript project of recent times.
I’ll cut to the chase, using uglifyjs -b
for example minification.
Visibility
This is a car that can turn left or right, and has a more general function turn
, that can turn any number of degrees.
function driver() {
var d = {};
d.direction = 0;
d.turn = function(degrees) {
d.direction += degrees;
};
d.right = function() { d.turn(90); };
d.left = function() { d.turn(-90); };
return d;
}
uglifyjs does its best: 147 chars, from 228: 64%
function driver() {
var a = {};
return a.direction = 0, a.turn = function(b) {
a.direction += b;
}, a.right = function() {
a.turn(90);
}, a.left = function() {
a.turn(-90);
}, a;
}
But see how this is calling a.turn()
internally? Often seeing non-mangled names means that there’s a tweak you can make. For instance, if you eliminate public access to d.turn
, by removing its assignment to d
:
function driver() {
var d = {};
d.direction = 0;
function turn(degrees) {
d.direction += degrees;
}
d.right = function() { turn(90); };
d.left = function() { turn(-90); };
return d;
}
Becomes
function driver() {
function b(b) {
a.direction += b;
}
var a = {};
return a.direction = 0, a.right = function() {
b(90);
}, a.left = function() {
b(-90);
}, a;
}
d.turn(degrees)
can now be mangled into just b
, so the compression becomes 131 chars from 220: 59% of its original size, by optimizing internal calls. It’s minor, in this instance, but add a lot of repetitive code and this difference can add up.
Aliasing
I was always mystified by how impressive libraries like underscore.js and reqwest would shortcut access to variables. After all, isn’t it just saving minor bytes to refer to doc_body
instead of document.body
?
It eventually dawned on me: minifiers aren’t always able to alias object properties. For instance,
|
|
However, if uglifyjs were to make the policy that it should alias all property-accessed objects in Javascript, then it would immediately fall into traps like:
|
|
Which gives you a big, fat ‘Illegal invocation’, since this
isn’t the document
when a
is run.
Now, to be clear, you could optimize to
var a = function() {
return document.getElementById.apply(document, arguments);
};
But that isn’t fun at all, and strictly proxies getElementById
- if the function has properties of its own, as they are plenty allowed to do in Javascript - then this optimization will kill them. So, it’s unsafe and big.
Moral of the story: minifiers don’t optimize object properties because it would be weird and hard for them to do it. When you use an object property a lot, make a variable for it yourself, and you can give that variable a descriptive name, like aliasToDocumentPropertyX
- so you can have both readability and good minification ratios.
The Little Things
Don’t care that much about ternary form versus if
/ else
. Local variables are reliably shortened, so don’t use single-char names unless you have very good reason, or they’re i
, x
or y
.
The old wisdom about always using var for local variables is here too: if you forget, your variable name is in the global scope, so
|
|
And so on
Don’t get caught up in minimization thinking it’s the golden ticket: it’s fun and useful, but ideally your javascript is cached - sometimes in compiled form - the first time it’s loaded.
But, if you’re writing code that aims to be invisible and uber-light for bad connections or to be thrown along with every page, it’s useful to grow an understanding of what your minifier can and can’t do.
I’m no minifier expert: if you’ve got any knowledge to share, please add it to comments or link it up. Have fun!