Dead code elimination and tree-shaking in JavaScript build systems

Update: I was wrong initially telling that tree-shaking operates on modules. There’a an article by Rich Harris, creator of Rollup tool, where he explains the difference between two approaches.

So Webpack 2 will be able to perform tree-shaking on ES2015 modules. Here’s a blog post explaining how to run a demo with Webpack 2 beta and Babel. I want to see here how well different build systems in JavaScript world can do tree-shaking and dead code elimination. Before we start, let’s learn what is the difference between those two techniques.

Dead code elimination (DCE) is a compiler optimisation which removes code which is never going to be executed or variables which are never used. Take a look at this example:

const x = 1;

if (false) {
console.log(x);
}

The code within if statement will never be executed here, so the whole statement can be stripped out. Then variable x is defined, but never used, so it also can be removed safely. Why compilers do this? To shrink the size of production code and reduce execution time, by removing unnecessary code.

Now tree-shaking does the same, but in opposite direction. Rather than excluding code, it includes only what is actually needed in the program. Turned out this is a much more efficient optimisation, which can dramatically reduce the size of the output. Consider the following example:

import { first } from 'lodash';

console.log(first([1, 2, 3]));

The lodash library includes a lot of useful functions, but even if only a single function is actually imported and used, the whole library’s code will end up in the final bundle. Tree-shaking will check for every module which exports are being used and will drop unused ones. If you wonder why it’s called tree-shaking: think of your application as a dependency graph, this is a tree, and each export is a branch. So if you shake the tree, the dead branches will fall. Now it makes sense.

From the article I linked above, Webpack 2 is processing two modules. I’ve modified main.js to see how tools will handle a case when a function was imported but not actually used.

// helpers.js
export function foo() { return 'foo'; }
export function bar() { return 'bar'; }

// main.js
import { foo, bar } from './helpers';
if (false) {
console.log(foo());
}
let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;

Here’s a link to formatted output. It includes Webpack’s loader wrapper, but the actual code looks like this and there’s no bar function and if statement as you can see:

([function(o, t, n) {
function r() {
return "foo"
}
t.foo = r
}, function(o, t, n) {
var r = n(0),
e = document.getElementById("output");
e.innerHTML = "Output: " + r.foo()
}]);

The other tool which does tree-shaking and DCE is rollup.js. It has a live demo on the their website. Rollup supports tree-shaking by default. The tool is only bundles your code, but not minifies it. Here’s the output, good job!

function foo() {
return 'foo';
}

let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;

Webpack itself doesn’t remove dead code, it only doesn’t export unused exports, so they become “dead”. After that UglifyJS removes that code and minifies everything.

The last tool is Google Closure Compiler. It’s been around for more than ten years already. Running it with advanced compilation level flag will produce the following output:

document.getElementById("output").innerHTML="Output: foo";

Closure Compiler is probably a total leader here. But Java is not welcome in JavaScript world. Unless you have big projects like Google’s Gmail, where this kind of optimisations makes a lot of sense.

Since Webpack is the most popular tool at the moment, it’s a very good news. Having tree-shaking capable build tool and libraries written in ES2015 modules we don’t need to worry about choosing one lib over the other because of its size. What we should do instead is to choose what fits better in our needs and let compiler do everything else.