Skip to main content

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/export are static; CommonJS and dynamic require() break the analysis.
  • Code with side effects is not removed, even when its exports are needed nowhere.
  • The sideEffects field in package.json tells the bundler it may prune modules more aggressively.
  • Tooling: Rollup, Webpack (version 2 and newer, in production mode), ESBuild, Vite.

Quick example

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

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

  1. Find the "roots", the modules the application actually uses (entry points).
  2. Build the import graph.
  3. 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:

text
Webpack / Rollup -> mark the unused exports | v Terser -> physically cuts the code out

Why ES Modules are required

Tree shaking works only with ES Modules (ESM), because they are static:

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

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

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

or selectively, listing only the files that really do have effects:

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

Configuration in bundlers

Webpack. The production mode enables tree shaking together with minification:

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

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

WhatDescription
Tree shakingRemoval of unused imports and exports
Works only withES Modules (import / export)
Does not removeCode with side effects
ConfigurationsideEffects: false in package.json, mode: production
ToolingWebpack ≥ 2, Rollup, ESBuild, Vite
GoalShrink 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:

ReasonWhy
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 modulesthey cannot be removed safely
Plugins or transpilation break ESMfor instance Babel turns import/export into require
mode: 'production' is not enabled in Webpackoptimisations 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": false blindly. 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 ready
Premium

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