Rich Harris’ module bundler Rollup popularized an important feature in the JavaScript world: tree-shaking, excluding unused exports from bundles. Rollup depends on the static structure of ES6 modules (imports and exports can’t be changed at runtime) to detect which exports are unused.
Tree-shaking for webpack is currently in beta. This blog post explains how it works. The project we are going to examine is on GitHub: tree-shaking-demo
webpack 2, a new version that is in beta, eliminates unused exports in two steps:
First, all ES6 module files are combined into a single bundle file. In that file, exports that were not imported anywhere are not exported, anymore.
Second, the bundle is minified, while eliminating dead code. Therefore, entities that are neither exported nor used inside their modules do not appear in the minified bundle. Without the first step, dead code elimination would never remove exports (registering an export keeps it alive).
Unused exports can only be reliably detected at build time if the module system has a static structure. Therefore, webpack 2 can parse and understand all of ES6 and only tree-shakes if it detects an ES6 module. However, only imports and exports are transpiled to ES5. If you want all of the bundle to be in ES5, you need a transpiler for the remaining parts of ES6. In this blog post, we’ll use Babel 6.
The demo project has two ES6 modules.
helpers.js
with helper functions:
// helpers.js
export function foo() {
return 'foo';
}
export function bar() {
return 'bar';
}
main.js
, the entry point of the web application:
// main.js
import {foo} from './helpers';
let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;
Note that the export bar
of module helpers
is not used anywhere in this project.
The canonical choice for Babel 6 is to use the preset es2015
:
{
presets: ['es2015'],
}
However, that preset includes the plugin transform-es2015-modules-commonjs
, which means that Babel will output CommonJS modules and webpack won’t be able to tree-shake:
function(module, exports) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.foo = foo;
exports.bar = bar;
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}
You can see that bar
is part of the exports, which prevents it being recognized as dead code by minification.
What we want is Babel’s es2015
, but without the plugin transform-es2015-modules-commonjs
. At the moment, the only way to get that is by mentioning all of the preset’s plugins in our configuration data, except for the one we want to exclude. The preset’s source is on GitHub, so it’s basically a case of copying and pasting:
{
plugins: [
'transform-es2015-template-literals',
'transform-es2015-literals',
'transform-es2015-function-name',
'transform-es2015-arrow-functions',
'transform-es2015-block-scoped-functions',
'transform-es2015-classes',
'transform-es2015-object-super',
'transform-es2015-shorthand-properties',
'transform-es2015-computed-properties',
'transform-es2015-for-of',
'transform-es2015-sticky-regex',
'transform-es2015-unicode-regex',
'check-es2015-constants',
'transform-es2015-spread',
'transform-es2015-parameters',
'transform-es2015-destructuring',
'transform-es2015-block-scoping',
'transform-es2015-typeof-symbol',
['transform-regenerator', { async: false, asyncGenerators: false }],
],
}
If we build the project now, module helpers
looks like this inside the bundle:
function(module, exports, __webpack_require__) {
/* harmony export */ exports["foo"] = foo;
/* unused harmony export bar */;
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}
Only foo
is an export now, but bar
is still there. After minification, helpers
looks like this (I’ve added line breaks and whitespace to make the code easier to read):
function (t, n, r) {
function e() {
return "foo"
}
n.foo = e
}
Et voilà – no more function bar
!