Web Performance Optimisation Techniques III - Tree Shaking

Tree shaking is a term borrowed from the concept of shaking a tree to remove dead leaves. In the context of web development, it refers to the process of eliminating unused or "dead" code from an application during the build process. This is particularly useful in JavaScript applications, where large libraries might be imported, but only a small portion of them is actually used.
The term was popularised by the JavaScript bundler Rollup, but it has since been adopted by other bundlers like Webpack, Parcel, and Vite. In this guide, we’ll explore the importance of tree shaking, how it works under the hood, and best practices to maximise its effectiveness.
Why is Tree Shaking Important?
Smaller Bundle Sizes:
One of the primary benefits of tree shaking is that it reduces the size of the final JavaScript bundle. Smaller bundles mean faster load times, which is crucial for user experience, especially on mobile devices or slow networks.
Improved Performance:
By removing unused code, applications become more efficient. Less code means less parsing, compiling, and executing, which leads to better runtime performance.
Dead Code Identification:
Highlights unused modules or dependencies during bundling, helping developers identify and remove dead code from source files. This reduces bloat and technical debt, ensuring a leaner, more maintainable codebase over time
The Mechanics of Tree shaking
To understand how tree shaking works, it is essential to dive into the mechanics behind it. At its core, tree shaking relies on static analysis, a process where the bundler analyses code without executing it.
Let’s break this down.
The Role of Static Analysis
Static analysis is the backbone of tree shaking. It allows the bundler to analyse code at build time and determine which parts are actually used. This is only possible with static code.
Static vs. Dynamic Code
Static Code: Code that can be analysed at build time or before the application runs. (e.g.,
importandexportstatements in ES6 modules).Dynamic Code: Code that cannot be analysed until runtime (e.g.,
requirestatements in CommonJS or dynamic imports).
Tree shaking works best with static code because the bundler can make decisions about what to include or exclude before the application runs.
How Tree Shaking Works
Tree shaking is a multi-step process that occurs during the build phase of an application. To better understand how this process works, let’s walk through a simple example step by step.
Consider the following two files:
math.js
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
main.js
// main.js
import { add, multiply } from './math.js';
console.log(add(5, 3)); // Only `add` is used
console.log(multiply(2, 4)); // Only `multiply` is used
In this example,
The
math.jsmodule exports four functions:add,subtract,multiply, anddivide.However, in
main.js, only theaddandmultiplyfunctions are imported and used.The goal of tree shaking is to ensure that only these two functions are included in the final bundle, while
subtractanddivideare removed as unused code.
Step-by-Step Process of Tree Shaking

Static Analysis of Imports and Exports
The bundler analyses all
importstatements in the code to identify which modules and functions are being used. It also examines theexportstatements to understand what is being exposed by each module.How Static Analysis Works
Static analysis is typically performed using an Abstract Syntax Tree (AST). An AST is a tree representation of the structure of the code, where each node corresponds to a construct in the source code (e.g., a function, variable, or expression).
For example, the AST for
math.jsmight look like this:{ "type": "Program", "body": [ { "type": "ExportNamedDeclaration", "declaration": { "type": "FunctionDeclaration", "id": { "name": "add" }, "params": [...], "body": [...] } }, { "type": "ExportNamedDeclaration", "declaration": { "type": "FunctionDeclaration", "id": { "name": "subtract" }, "params": [...], "body": [...] } }, ... ] }The bundler uses the AST to:
Identify which functions are exported from
math.js(add,subtract,multiply, anddivide).Determine which functions are imported and used in
main.js(addandmultiply).
By analysing these statements, the bundler can determine which functions are used and which are not.
Building a Dependency Graph
Once the bundler has analysed the imports and exports, it constructs a dependency graph. This graph represents the relationships between modules and functions in the application.
Nodes: Each module or function is a node in the graph.
Edges: The edges represent dependencies between modules (e.g.,
main.jsdepends onmath.js).
The dependency graph helps the bundler understand which parts of the code are reachable from the entry point of the application (e.g., main.js).
For example, the dependency graph for the above code might look like this:
main.js
└── math.js
├── add
└── multiply
In this graph:
main.jsis the entry point.math.jsis a dependency ofmain.js.Only
addandmultiplyare connected to the entry point (main.js), meaning they are used in the application.
The subtract and divide functions are not connected to the entry point, so they are considered unreachable.
Dead Code Elimination
After building the dependency graph, the bundler identifies and removes any code that is not reachable from the entry point. This process is known as dead code elimination.
Reachable Code: Code that is directly or indirectly referenced from the entry point is kept.
Unreachable Code: Code that is not referenced (e.g., unused functions or modules) is removed.
In the example above, the subtract and divide functions are unreachable and will be eliminated.
Challenges with Tree Shaking
While tree shaking is a powerful optimisation technique, it is not without its challenges:
Dynamic Code
Because tree shaking relies on static analysis, it cannot determine which parts of dynamically loaded modules will be used. As a result, the bundler may include the entire module in the final bundle, even if only a small portion is needed.
Example: Dynamic Imports
import('./math.js').then(math => { console.log(math.add(2, 3)); });In this case:
The bundler cannot determine which functions from
math.jswill be used at runtime.As a result, it may include the entire
math.jsmodule in the bundle, even if only theaddfunction is needed.
Another Example: CommonJS Modules
CommonJS is a module system used in Node.js and older JavaScript projects. It uses require and module.exports to define and import modules. However, CommonJS is inherently dynamic, which makes tree shaking difficult or impossible.
Let’s rewrite our earlier example to use commonJS:
math.js
// math.js
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;
main.js
// main.js
const math = require('./math.js');
console.log(math.add(2, 3)); // Only `add` is used
console.log(math.multiply(2, 4)); // Only `multiply` is used
In this example:
The
requirestatement dynamically loads themath.jsmodule.The bundler cannot determine which functions (
add,subtract,multiply,divide) will be used at runtime.As a result, the entire
math.jsmodule is included in the bundle, even though onlyaddandmultiplyare used.
Side Effects
Code with side effects cannot be safely removed, even if it appears unused.
For example:
// side-effect.js console.log('This is a side effect!'); // main.js import './side-effect.js';Even though
side-effect.jsdoes not export anything, theconsole.logstatement is a side effect, so it will be included in the bundle.
Best Practices for Effective Tree Shaking
To get the most out of tree shaking, follow these best practices:
Use ES6 Modules
- Whenever possible, use ES6 modules (
import/export) instead of CommonJS (require/module.exports). ES6 modules have a static structure that enables effective tree shaking.
- Whenever possible, use ES6 modules (
Avoid Dynamic Imports
- Use static imports whenever possible. Reserve dynamic imports for cases where lazy loading is absolutely necessary.
Minimise Side Effects
Whenever possible, try to write pure functions as they are easier to tree shake because they don’t have side effects.
However, If your code has side effects, use the
sideEffectsproperty inpackage.jsonto indicate which files have side effects. This helps the bundler make better decisions about what to include or exclude.Example:
//package.json { "sideEffects": [ "src/some-side-effectful-file.js", "*.css" ] }
Optimise Third-Party Libraries
Look for libraries that support ES6 modules and have minimal side effects.
Also, use tools like Babel or Rollup to convert a third-party library that uses CommonJS to ES6 modules.
Popular bundlers That Support Tree Shaking
Below is a table summarising the popular bundlers that support tree shaking:
| Bundler | Key Features |
| Webpack | Requires mode: 'production' and sideEffects property in package.json. |
| Rollup | First bundler to introduce tree shaking. Automatically performs tree shaking with ES6 modules. |
| Parcel | Zero-configuration bundler with built-in tree shaking support. |
| Vite | Uses ESBuild for fast bundling and Rollup for production builds. Supports tree shaking during development and production. |
In Conclusion,
Tree shaking is a powerful technique for optimizing JavaScript applications. By eliminating unused code, it significantly reduces bundle sizes, improves performance, and enhances maintainability. While tree shaking is most effective with ES6 modules, it’s important to follow best practices and be aware of common pitfalls to get the most out of it.





