Skip to main content

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

javascript
// 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:

  • multiply is never imported anywhere,
  • so it can be safely removed.

Only this remains in the resulting bundle:

javascript
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:

javascript
// 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:

  1. It identifies the "roots": the modules actually used in the app (entry points);
  2. It builds the import graph;
  3. 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:

javascript
Webpack / Rollup → mark unused exports ↓ Terser → physically strip the code

Example: side effects

Tree shaking does not remove code with side effects: anything that can change global state.

javascript
// 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:

javascript
{ "name": "my-lib", "sideEffects": false }

or selectively:

javascript
{ "sideEffects": [ "*.css", "./polyfills.js" ] }

Common reasons tree shaking doesn't work

ReasonWhy
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 modulesthey cannot be safely removed
Plugins or transpilation break ESMfor example, Babel turns import/export into require
mode: 'production' is not enabled in Webpackoptimizations are off in dev mode

Example: Webpack

javascript
// 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:

javascript
// 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

WhatDescription
Tree shakingRemoves unused imports/exports
Works only withES Modules (import / export)
Does not removeCode with side effects
ConfigurationsideEffects: false in package.json, mode: production
ToolsWebpack >= 2, Rollup, ESBuild, Vite
GoalReduce 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.