How does "tree shaking" work?
What is Tree Shaking
Tree shaking is the process of analyzing module imports/exports and removing the ones that are not used in the final code.
Put simply:
If a function, class, or variable is never called / never imported, the bundler just drops it from the resulting bundle.
The name is a metaphor: the tree = the project's dependency graph, shaking = "shaking off" the unused branches.
Example without tree shaking
// 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, the bundle would include both add and multiply,
even though multiply is never used.
Example with tree shaking
A modern bundler (Rollup, Webpack 5+, ESBuild, Vite) analyzes:
multiplyis never imported anywhere,- so it can be safely removed.
Only this remains in the resulting bundle:
function add(a,b){return a+b}
console.log(add(2,3))Conditions for tree shaking to work
Tree shaking works only with ES Modules (ESM), because they are static:
// ESM
import { add } from './math.js';
// CommonJS (analysis is impossible at build time)
const math = require('./math');The bundler must see all imports/exports at compile time, not at runtime (dynamic require() calls break the optimization).
How the bundler figures out what can be removed
Tree shaking is an analysis of the dependency graph:
- It identifies the "roots": the modules actually used in the app (entry points);
- It builds the import graph;
- It removes everything that is unreachable from those roots.
"Dead Code Elimination" vs "Tree Shaking"
Tree shaking is a logical analysis of imports/exports at the module level. Dead code elimination (DCE) is the follow-up analysis at the expression level (usually done by Terser/UglifyJS).
The typical chain looks like this:
Webpack / Rollup → mark unused exports
↓
Terser → physically strip the codeExample: side effects
Tree shaking does not remove code with side effects: anything that can change global state.
// utils.js
console.log('module loaded'); // a side effect!
export const a = 1;
export const b = 2;Even if you don't import a or b, the bundler will not remove the module
if it sees a side effect (a console.log call, changing a global variable, and so on).
A hint for the bundler: package.json → sideEffects
In a package you can explicitly state that the code has no side effects, and tree shaking then works more aggressively:
{
"name": "my-lib",
"sideEffects": false
}or selectively:
{
"sideEffects": [
"*.css",
"./polyfills.js"
]
}Common reasons tree shaking doesn't work
| Reason | Why |
|---|---|
CommonJS is used (require) | there's no static dependency analysis |
Dynamic imports (import * as m) or require(variable) | the bundler can't predict what exactly is imported |
| Side effects in modules | they cannot be safely removed |
| Plugins or transpilation break ESM | for example, Babel turns import/export into require |
mode: 'production' is not enabled in Webpack | optimizations are off in dev mode |
Example: Webpack
// webpack.config.js
module.exports = {
mode: 'production', // enables tree-shaking + minify
optimization: {
usedExports: true, // marks used exports
}
};If you look in a bundle analyzer, you'll see comments like /* unused harmony export */: that's tree shaking in action.
Example: Rollup (the ideal case)
Rollup was originally built around tree shaking, so it does this better than anyone:
// rollup.config.js
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
}
};Rollup removes unused exports and even "chains" of functions if they are never called.
Integration with Vite / ESBuild
Vite and ESBuild always work with ESM and support tree shaking out of the box: there's nothing to configure.
The main thing: don't use CommonJS and don't have hidden side effects.
Summary
| What | Description |
|---|---|
| Tree shaking | Removes unused imports/exports |
| Works only with | ES Modules (import / export) |
| Does not remove | Code with side effects |
| Configuration | sideEffects: false in package.json, mode: production |
| Tools | Webpack >= 2, Rollup, ESBuild, Vite |
| Goal | Reduce bundle size without manually removing code |
The main idea:
A bundler builds a "dependency tree" and "shakes off" everything that isn't used, hence "tree shaking".
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.