What is tree shaking and how does it work?

When Javascript applications get to a certain size, it’s helpful to separate the code into modules. However, when we do so, we can end up with code imported that isn’t actually used. Tree shaking is a method of optimising our code bundles by eliminating any code from the final file that isn’t actually being used.

Let’s say we have a utilities file with some math operations we may want to use in our main script.

mathUtils.js
export function add(a, b) {
    console.log("add");
    return a + b;
}

export function minus(a, b) {
    console.log("minus");
    return a - b;
}

export function multiply(a, b) {
    console.log("multiply");
    return a * b;
}

export function divide(a, b) {
    console.log("divide");
    return a / b;
}

In our main script, we may only ever import and use the add() function.

index.js
import { add } from "./mathUtils";

add(1, 2);

Assuming we are using a tool like webpack to create our bundle, we will see that all functions from the mathUtils.js file are included in the final bundle, even though we only imported and used the add() function.

bundle.js
/***/ "./src/mathUtils.js":
/*!**************************!*\
  !*** ./src/mathUtils.js ***!
  \**************************/
/*! exports provided: add, minus, multiply, divide */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"add\", function() { return add; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"minus\", function() { return minus; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"multiply\", function() { return multiply; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"divide\", function() { return divide; });\nfunction add(a, b) {\n    console.log(\"add\");\n    return a + b;\n}\n\nfunction minus(a, b) {\n    console.log(\"minus\");\n    return a - b;\n}\n\nfunction multiply(a, b) {\n    console.log(\"multiply\");\n    return a * b;\n}\n\nfunction divide(a, b) {\n    console.log(\"divide\");\n    return a / b;\n}\n\n\n//# sourceURL=webpack:///./src/mathUtils.js?");

/***/ })

With tree shaking enabled, only what is imported and actually used will make it to the final bundle.

How does tree shaking work?

Although the concept of tree shaking has been around since at least the 1990s, it has only been able to work with Javascript since the introduction of ES6-style modules. This is because tree shaking can only work if the modules are "static".

Before ES6 modules, we had CommonJS modules which used the require() syntax. These modules were "dynamic", meaning that we could import new modules based on conditions in our code.

var myDynamicModule;

if (condition) {
    myDynamicModule = require("foo");
} else {
    myDynamicModule = require("bar");
}

This dynamic nature of the CommonJS modules meant that tree shaking couldn’t be applied because it would be impossible to determine which modules will be needed before the code is actually run.

In ES6, a new syntax for modules was introduced, which was entirely static. Using the import syntax, we can no longer dynamically import a module.

if (condition) {
    import foo from "foo";
} else {
    import bar from "bar";
}
Doesn't work

Instead, we have to define all imports in the global scope, outside of any conditions.

import foo from "foo";
import bar from "bar";

if (condition) {
    // do stuff with foo
} else {
    // do stuff with bar
}

Among other benefits, this new syntax allows for effective tree shaking, as any code that is used from an import can be determined without the code needing to be first run.

What does tree shaking shake off?

Tree shaking, at least webpack's implementation of the feature, is pretty good at eliminating as much unused code as possible. For example, imports that are import-ed but not used are eliminated.

import { add, multiply } from "./mathUtils";

add(1, 2);

In the above example, the multiply() function is never used and will be removed from the final bundle.

Even specific properties from imported objects that are never accessed are removed.

myInfo.js
export const myInfo = {
    name: "Ire Aderinokun",
    birthday: "2 March"
}
index.js
import { myInfo } from "./myInfo.js";

console.log(myInfo.name);

In the above example, the birthday property never makes it to the final bundle because it is never actually used.

However, tree shaking doesn’t eliminate all unused code. The details of what is and isn’t eliminated is beyond the scope of this article, but it should be noted that the use of tree shaking doesn’t solve the problem of unused code entirely.

What about "side effects"?

A side effect is code that performs some action when imported, not necessarily related to any export. A good example of a side effect is a polyfill. Polyfills typically don’t provide an export to be used in the main script, but rather add to the project as a whole.

Tree shaking can’t automatically tell which scripts are side effects, so it’s important to specify them manually, as we will see below.

How to tree shake

Tree shaking is typically implemented via a code bundler. If you’re using webpack, for example, you can simply set the mode to production in your webpack.config.js configuration file. This will, among other optimisations, enable tree shaking.

module.exports = {
    ...,
    mode: "production",
    ...,
};

To mark certain files as side effects, we need to add them to our package.json file.

{
    ...,
    "sideEffects": [
        "./src/polyfill.js"
    ],
    ...,
}

For more information on how to implement tree shaking using webpack, check out their documentation.

blog comments powered by Disqus