Skip to main content

Tree Shaking

What is tree shaking ?

Tree shaking is a term popularized by Rollup which at its core is a kind of dead code elimination.

As an app grows, its dependencies keeps growing and older dependencies fall out of use, but may not get pruned from your codebase. This results in shipping lots of unused Javascrip code.

Before ES6, CommonJS is widely used to address the module handling issue. It provides a require() function call that fetches an external module based on the path provided and adds it to the scope during runtime.

Given require is a function call, it supports dynamic import and can be called anywhere in the code. This, however, makes it hard to predict the codes behaviour at runtime. Take below code for example, both admin.js and user.js module code can't be removed since bundler can't predict the result of isAdmin. It's DYNAMIC !!!. Bundler will have to include both code in the result bundle.

const isAdmin = await isAuthorizedAdmin();
let resource = null;

if(isAdmin){
resource = require('admin.js');
} else {
resource = require('user.js');
}

Tree shaking addresses the issue by taking advantage of static import statements import and export introduced in ES6, which can only be used as top-level declarations, meaning nesting them in any other structure is not possible, making it static and does not depend on runtime execution.

// Import all the array utilities and can't be tree shaked!
import arrayUtils from "array-utils";

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

Contrast to the first syntax, second syntax can serve as a hint to the bundler as which part of the imported module is actually used. Through static analysis, bundlers that support tree shaking can build up an dependency graph from the entry point of the program, and gradually drop code blocks that are unreachable. This process is also known as dependency resolution.

Tree shaking in Webpack

If Webpack is used for bundling, the entire module still gets imported in development mode. In production mode, however, Webpack can be configured to "shake" off exports from ES6 modules that weren't explicitly imported, making those production builds smaller. This can be set through the webpack.config.js file:

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

Side Effects

Side effect is defined as code that performs a special behavior when imported, other than exposing one or more exports. A function is considered pure if it has no side effects, meaning it always yields the same result, regardless of context or the environment it’s executed in; otherwise, it is considered an inpure function.

const pureFn = (a, b) => a + b
const impureFn1 = (c) => window.foo.number + c
const impureFn2 = () => {
window.customFunction = ()=> {...};
}

An example is a polyfill, which usually do not provide an export but adds some functionality to the global scope.

For bundlers, however, they have to be conservative on whether a block of code can be safely removed. If an imported module has side effects, bundler must include it to avoid breaking the code.

Thus, modules that have side effect must be specified manually. To mark certain files as side effects, configuration must be added to the package.json file.

// This tells the bundler that the package has no side
// effect and unreachable code can be removed.
{
"sideEffects": false
}

// This tells the bundler that polyfill.js has side effect and should not be removed
// even though unreachable on static analysis.
{
"sideEffects": [
"./src/polyfill.js"
]
}

Side-effect free can also be flagged on a statement level. With Webpack, statement can be marked as side-effect free with below annotation.

const x = */@__PURE__*/pure_function_call();

Visulization Tools

As project gets larger, it might be hard to trace where exactly the bloat in size is coming from, and third-party tools such as Webpack Bundle Analyzer and source-map-explorer can provide further insight through visualizing the size of output files in a treemap.

Advanced

Keeping Babel from transpiling ES6 modules to CommonJS modules

If '@babel/preset-env' is used, Babel may transform ES6 modules into more widely compatible CommonJS modules, which makes it a dynamic import and tree shaking won't be applicable. The solution is to configure @babel/preset-env to explicitly leave ES6 modules as is. which can be configured through `babel.json. getatas

References