Tree shaking
Tree shaking is the process of analysing module imports and exports and removing the ones that are not used in the final code. Put simply: if a function, class or variable is never called and never imported, the bundler just throws it out of the resulting bundle.
Theory
TL;DR
- The name is a metaphor: the tree is the project's dependency graph, and shaking is knocking the unused branches off it.
- The bundler takes the entry points, builds an import graph and removes everything unreachable from those roots.
- It only works with ES Modules, because
import/exportare static; CommonJS and dynamicrequire()break the analysis. - Code with side effects is not removed, even when its exports are needed nowhere.
- The
sideEffectsfield inpackage.jsontells the bundler it may prune modules more aggressively. - Tooling: Rollup, Webpack (version 2 and newer, in
productionmode), ESBuild, Vite.
Quick 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));Without tree shaking, both add and multiply end up in the bundle, even though multiply is never used.
A modern bundler (Rollup, Webpack 5 and newer, ESBuild, Vite) will see that multiply is imported nowhere, so it can be safely removed. What is left in the resulting bundle is only:
function add(a,b){return a+b}
console.log(add(2,3))How the bundler knows what it may remove
Tree shaking is dependency graph analysis in three steps:
- Find the "roots", the modules the application actually uses (entry points).
- Build the import graph.
- Remove everything unreachable from those roots.
Two neighbouring terms are worth separating. Tree shaking is the logical analysis of imports and exports at the module level. Dead code elimination (DCE) is the follow-up analysis at the expression level, usually done by the minifier (Terser, UglifyJS). The chain looks like this:
Webpack / Rollup -> mark the unused exports
|
v
Terser -> physically cuts the code outWhy ES Modules are required
Tree shaking works only with ES Modules (ESM), because they are static:
// Works: ESM
import { add } from './math.js';
// Does not work: CommonJS, build-time analysis is impossible
const math = require('./math');The bundler has to see every import and export at compile time, not at run time. A dynamic require() with a variable in the path breaks the optimisation, because there is no way to predict what will actually be imported.
Side effects and the sideEffects field
Tree shaking does not remove code with side effects, that is code that may change global state:
// utils.js
console.log('module loaded'); // a side effect
export const a = 1;
export const b = 2;Even if you import neither a nor b, the bundler will not remove the module, because it sees a side effect: a console.log call, a change to a global variable and so on.
A package can state explicitly that its code has no side effects, and then tree shaking works more aggressively:
{
"name": "my-lib",
"sideEffects": false
}or selectively, listing only the files that really do have effects:
{
"sideEffects": [
"*.css",
"./polyfills.js"
]
}Configuration in bundlers
Webpack. The production mode enables tree shaking together with minification:
// webpack.config.js
module.exports = {
mode: 'production', // enables tree shaking and minification
optimization: {
usedExports: true, // marks the used exports
}
};If you look into bundle-analyzer, you will see /* unused harmony export */ comments, which is tree shaking in action.
Rollup. It was built around tree shaking from the start, so it does the job best:
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
}
};Rollup removes not only unused exports but whole chains of functions when nobody calls them.
Vite and ESBuild. They always work with ESM and support tree shaking out of the box, with nothing to configure. The main thing is to avoid CommonJS and hidden side effects.
Summary:
| What | Description |
|---|---|
| Tree shaking | Removal of unused imports and exports |
| Works only with | ES Modules (import / export) |
| Does not remove | Code with side effects |
| Configuration | sideEffects: false in package.json, mode: production |
| Tooling | Webpack ≥ 2, Rollup, ESBuild, Vite |
| Goal | Shrink the bundle without deleting code by hand |
The main idea: the bundler builds a "dependency tree" and shakes off everything that is not used, hence the name tree shaking.
Common mistakes
The most common reasons tree shaking fails to kick in:
| Reason | Why |
|---|---|
CommonJS is used (require) | there is no static dependency analysis |
Importing the whole module (import * as m) or require(variable) | the bundler cannot predict what exactly is imported |
| Side effects in modules | they cannot be removed safely |
| Plugins or transpilation break ESM | for instance Babel turns import/export into require |
mode: 'production' is not enabled in Webpack | optimisations are off in dev mode |
A few more things worth remembering:
- Expecting tree shaking to shrink a dev build. Bundle size should only ever be measured on a production build.
- Setting
"sideEffects": falseblindly. If the package has CSS imports, polyfills or global helper registration, the bundler will drop them and the app breaks in production although everything worked locally. - Confusing tree shaking with code splitting. The first removes unused code, the second splits the used code into separate chunks; they are different optimisations.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.