Tree shaking is the process of removing unused code (“dead code elimination”) from JavaScript bundles by leveraging the static import
/export
structure of ES modules. In practice, bundlers parse your code into an Abstract Syntax Tree (AST), follow import/export links, and mark which exports are actually used. Unreferenced functions or variables can then be omitted from the final bundle.
It’s a crucial optimization for frontend performance, especially for applications depending on large libraries like Lodash, D3, or Moment.js.
🔍 How Tree Shaking Works
Tree shaking relies on the ability to statically analyze code. This is why it works well with:
- ES Modules (ESM): Static
import/export
syntax is required. - Bundlers: Tools like Webpack, Rollup, esbuild, and Vite use ASTs to determine which exports are unused.
✅ Shakeable Code Example
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.js
import { add } from './math.js';
console.log(add(2, 3)); // Only `add` should be kept in the bundle
If your bundler is configured correctly, the multiply
function will be removed from the final build.
❌ Unshakeable Code Example
// utils.js
export const utils = {
isString(val) {
return typeof val === 'string';
},
isNumber(val) {
return typeof val === 'number';
}
};
// app.js
import { utils } from './utils.js';
console.log(utils.isString('hello'));
In this case, both isString
and isNumber
are retained because utils
is an object that references both. The bundler can’t know that isNumber
is unused.
📦 Tree Shaking in Popular Libraries
Lodash
Lodash traditionally exposes functions as part of a single object:
import _ from 'lodash';
This import pulls in the entire library (~70kb gzipped). Even if you only use _.debounce
, everything comes along.
Shakeable Alternative
import debounce from 'lodash/debounce';
Or better yet:
import { debounce } from 'lodash-es'; // ESM-compatible build
lodash-es
allows true tree shaking because it uses native ESM exports.
D3
D3 is a famously large and modular library. The main d3
package is a bundle of ~30 submodules.
import * as d3 from 'd3';
This includes everything.
Instead:
import { scaleLinear } from 'd3-scale';
Modular imports enable bundlers to remove unused parts of D3.
React
React itself isn’t tree-shakeable in the same way because you typically import the whole API:
import React from 'react';
However, React’s internal dev-only code is removed via process.env.NODE_ENV
checks during bundling (e.g., with Webpack + Babel). Also, React v18+ modular architecture (like react-dom/client
) enables finer granularity.
✅ Best Practices for Writing Shakeable Code
Use ESM syntax: Avoid
require()
andmodule.exports
.Avoid barrel files: Re-exporting everything from
index.js
makes it harder for bundlers to analyze.Avoid deep object exports:
export const utils = { a, b, c }; // bad export { a, b, c }; // good
No side effects: Code with side effects can’t be safely removed.
Example:
import './styles.css'; // not tree-shakeable
Add
"sideEffects": false
to yourpackage.json
(or list specific files).
🛠 Bundler Differences
Feature | Webpack | Rollup | esbuild |
---|---|---|---|
Tree Shaking | Yes | Yes (very good) | Yes (fastest) |
ESM Support | Partial (better in v5) | Excellent | Excellent |
Side Effects | Needs config (sideEffects ) |
Auto detection | Auto detection |
Performance | Medium | Slow | Extremely Fast |
Each bundler’s approach and these examples illustrate both the power and the caveats of tree shaking in practice.