Tree shaking: dropping the code nobody imports from the bundle

Tree shaking removes from the final file every export no module uses. Here is why it requires ES modules, and what commonly stops it from working.
3 min read
Believemy logo

You import a single function from a library that offers two hundred. The other hundred and ninety-nine have no reason to travel to the visitor's browser.

Tree shaking exists for exactly that: shake the dependency tree and let the dead branches fall.


Definition

Tree shaking is the removal, at build time, of exported code no file imports. A Bundler builds the import graph, marks what is actually reached, then writes the final file without the rest.

JAVASCRIPT
// utils.js
export function formatPrice(value) {
  return value.toFixed(2) + " EUR";
}

export function generateInvoicePdf(order) {
  // 40 KB of dependencies pulled in here
  return order;
}

// main.js
import { formatPrice } from './utils.js';

console.log(formatPrice(12.5)); // 12.50 EUR

// In the produced file: generateInvoicePdf is gone,
// and its 40 KB of dependencies with it.

The win rarely comes from your own code. It comes from dependencies, where a badly aimed import drags a whole library along for three useful lines.

The weight shipped to the browser before and after tree shakingBefore the build, 42 KB of which 40 belong to a function nobody imports. After, 2 KB are left, 21 times less.What the build keeps, what it dropsBefore the build: 42 KB with dependenciesgenerateInvoicePdf and its 40 KBnobody imports it: droppedThe bundler keeps what is imported, drops the restAfter the build: 2 KB in the bundleformatPrice, the only imported function21 times less code sent to the browser


Why it only works with ES modules

A ES module declares its imports and exports in a fixed way, at the top of the file, with no conditions. The tool can therefore decide what is used before anything runs.

With CommonJS, require() is an ordinary call whose path can be computed and whose result can be modified along the way. No reliable analysis is possible, so nothing is removed. It is the strongest technical argument for ES modules in new code.

Good to know

An import of the form import * as everything from './utils.js' followed by a dynamic property access makes analysis impossible: the bundler cannot know which property will be read, so it keeps all of them.


What blocks the pruning

  • Side effects: a module that modifies a global object or installs a listener while loading cannot safely be removed, even when none of its exports are used.
  • Doubt: when unsure, a bundler keeps. Its priority is breaking nothing, not saving a kilobyte.
  • A missing declaration: the "sideEffects": false field of a package.json announces that a package can be pruned safely. Without it, the tool stays cautious.


Frequently asked questions

Question

Do I have to turn tree shaking on?

It is already on in the production configuration of the usual tools. The real question is which conditions must be met for it to bite: ES modules, named imports, and dependencies published in an analyzable format.


Question

How do I check that it worked?

By looking at the bundle composition report every bundler can produce. It shows the real weight of each module in the final file. Searching the minified file for a function name proves nothing, since internal names have changed.


Question

Why does a library stay whole in my bundle?

Three main causes: it is published in CommonJS only, it is imported wholesale rather than through named exports, or it runs code while loading. Many packages offer an ES module build: that is the one to aim for.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.