Tom MacWright

tom@macwright.com

Writing Javascript for Size

Update: This post was published in 2011. As of 2017,
  • 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,

function logStyles() {
    var doc_style = document.body.style;
    console.log(doc_style.border,
        doc_style.margin,
        doc_style.padding);
}
// after uglify -b, line-broken
function logStyles() {
    var a = document.body.style;
    console.log(a.border,
        a.margin,
        a.padding);
}

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:

// unminified
var mine = document.getElementById('mine');
var his = document.getElementById('his');
var hers = document.getElementById('hers');
// theoretical naïve minification
var a = document.getElementById;
var mine = a('mine'),
    his = a('mine'),
    hers = a('mine');

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

// unminified
function forgetVar() {
    myLongVariableName = 2;
}

function dontForgetVar() {
    var myVeryVeryLongVariableName = 2;
}
// after uglify -b
function forgetVar() {
    myLongVariableName = 2;
}

function dontForgetVar() {
    var a = 2;
}

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!