QuestionsWeb Performance

Tree-shaking and shipping less JavaScript

Tree-shakingMediumWeb Performance

You import one function from a utility library but the whole thing ends up in your bundle. Why, and how does tree-shaking prevent it?

What it tests

Understanding dead-code elimination, why it depends on ESM and side effects, and common defeats.

Approach & answer

Tree-shaking is dead-code elimination for JavaScript modules: the bundler keeps only the exports you actually use and drops the rest. It relies on ES modules (import/export) being STATICALLY analyzable — the bundler can see at build time exactly which named exports are reached from your entry point and prune everything unreachable. It fundamentally cannot work the same way on CommonJS (require) because require is dynamic (you can require a computed string, reassign exports), so a CJS-only library often pulls in wholesale. So the first reason 'one function drags in the whole library' is that the library ships CommonJS, or you imported it in a way that isn't shakeable. The second reason is SIDE EFFECTS: if a module runs code at import time (patches a global, registers something), the bundler must keep it even if you use none of its exports — unless the package declares `"sideEffects": false` (or lists the few files that do have them) in package.json, which grants the bundler permission to drop unused modules. Practical rules: (1) prefer libraries that ship ESM and are marked side-effect-free; (2) import only what you need with named imports (import { debounce } from 'lib') rather than a namespace import of everything, and avoid re-export barrel files that accidentally force-load siblings; (3) make sure minification/production mode is on, since tree-shaking's removal is finalized by the minifier; (4) verify with a bundle analyzer — the honest measure is what actually ends up in the output, not what you intended. The payoff is fewer bytes to download AND less JS to parse/execute, which helps TTI and INP.

Use this technique when

Explaining why a bundle is bigger than expected; choosing/importing libraries for shakeability.

Code

// Shakeable: named import from an ESM, side-effect-free library
import { debounce } from 'lodash-es';   // only debounce (+deps) is bundled

// Often NOT shakeable: default/namespace import pulls the whole thing
import _ from 'lodash';                  // entire library
_.debounce(fn, 200);

// package.json of a library declaring it's safe to drop unused modules:
// {
//   "sideEffects": false,        // or ["./src/polyfill.js"]
//   "module": "dist/index.esm.js" // ships an ESM build
// }

References