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.,
import
andexport
statements in ES6 modules).Dynamic Code: Code that cannot be analysed until runtime (e.g.,
require
statements 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.js
module exports four functions:add
,subtract
,multiply
, anddivide
.However, in
main.js
, only theadd
andmultiply
functions are imported and used.The goal of tree shaking is to ensure that only these two functions are included in the final bundle, while
subtract
anddivide
are removed as unused code.
Step-by-Step Process of Tree Shaking
Static Analysis of Imports and Exports
The bundler analyses all
import
statements in the code to identify which modules and functions are being used. It also examines theexport
statements 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.js
might 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
(add
andmultiply
).
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.js
depends 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.js
is the entry point.math.js
is a dependency ofmain.js
.Only
add
andmultiply
are 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.js
will be used at runtime.As a result, it may include the entire
math.js
module in the bundle, even if only theadd
function 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
require
statement dynamically loads themath.js
module.The bundler cannot determine which functions (
add
,subtract
,multiply
,divide
) will be used at runtime.As a result, the entire
math.js
module is included in the bundle, even though onlyadd
andmultiply
are 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.js
does not export anything, theconsole.log
statement 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
sideEffects
property inpackage.json
to 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.
Subscribe to my newsletter
Read articles from MyCodingNotebook directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

MyCodingNotebook
MyCodingNotebook
MyCodingNotebook is every developer’s online notebook—a space to explore coding concepts, share insights, and learn best practices. Whether you're just starting or an experienced developer, this blog serves as a go-to resource for coding best practices, real-world solutions, and continuous learning. Covering topics from foundational programming principles to advanced software engineering techniques, MyCodingNotebook helps you navigate your coding journey with clarity and confidence. Stay inspired, keep learning, and continue growing in your coding journey.