JavaScript bundles are getting huge, and a big reason is all the code that ships but never actually runs. Tree shaking is the process of identifying and removing this dead code, making your application faster and more efficient.
Here’s a simple example of how it works:
Imagine you have a utility file with several functions:
// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
And in your main application file, you only use add:
// app.js
import { add } from './utils.js';
console.log(add(5, 3));
When a bundler like Webpack or Rollup performs tree shaking, it analyzes app.js, sees that only add is imported and used. It then determines that subtract and multiply are unused and can be safely removed from the final bundle. The resulting bundle would effectively only contain the add function and the code that calls it.
This process is crucial for modern JavaScript development, especially with the rise of component-based frameworks and extensive use of third-party libraries. Libraries often export many functions, but your application might only need a small subset. Without tree shaking, you’d be shipping the entire library, including all its unused parts.
The magic behind tree shaking relies on static analysis of your code. Bundlers read your import and export statements. Because JavaScript’s module system (ES Modules) is designed to be statically analyzable, bundlers can reliably determine which code is being imported and used. They build a dependency graph, tracing the path from your entry point through all imported modules. Any module or piece of code within a module that isn’t reachable from the entry point is considered dead code and is pruned.
The exact levers you control are primarily through your bundler’s configuration and how you write your code.
Bundler Configuration:
Most modern bundlers have tree shaking enabled by default, but you might need to ensure it’s active. For Webpack, this is typically handled by setting mode: 'production' in your webpack.config.js. Production mode enables optimizations like minification and tree shaking. For Rollup, tree shaking is a core feature and is usually active when you build for production.
Code Structure:
- Use ES Modules (
import/export): Tree shaking works best with ES Modules. Avoid CommonJS (require/module.exports) for code that you want to be shaken, as it’s dynamically evaluated and harder for static analysis. - Side-effect-free Modules: A module has side effects if it performs actions when imported that don’t involve returning a value (e.g., modifying global variables, logging to the console, making network requests). Bundlers are cautious about removing modules with side effects because even if no value is explicitly used, the side effect might be intended. If a module has side effects, it might not be shaken even if its exports aren’t directly referenced. You can often tell your bundler which modules are side-effect-free (e.g., using
sideEffects: falseinpackage.jsonfor libraries, or configuring specific rules in Webpack).
Consider this scenario:
// utils.js
export const PI = 3.14159;
export function calculateArea(radius) {
return PI * radius * radius;
}
export function calculateCircumference(radius) {
return 2 * PI * radius;
}
And in app.js:
// app.js
import { calculateArea } from './utils.js';
console.log('Area:', calculateArea(5));
A bundler will see that PI and calculateCircumference are exported from utils.js, but only calculateArea is imported and used in app.js. Therefore, PI and calculateCircumference can be removed from the final bundle. The bundler might even be smart enough to see that PI is only used by calculateArea and could potentially remove PI as well if it’s not exported directly or if the bundler can inline it.
The most surprising thing about tree shaking is that it doesn’t just remove entire unused files; it can also remove individual functions or even specific lines of code within a file if they are not used. This fine-grained removal is possible because bundlers can analyze the Abstract Syntax Tree (AST) of your code, allowing them to understand the structure and dependencies at a very granular level. For example, if a library exports a large object with many methods, and you only import one or two of those methods, tree shaking can often strip away all the other unused methods from the exported object.
The next hurdle you’ll often face is understanding how dynamically imported modules (using import()) interact with tree shaking, and how to ensure your code splitting strategy aligns with dead code elimination.