Reference guide · performance · Published 2026-08-16 · 3 min read
How tree shaking removes dead JavaScript and why it can fail
Tree shaking and dead code elimination in JS bundlers, side-effect detection, and pitfalls that silently keep code.
- ·How it works
- ·What blocks it
- ·Enable it fully
Tree shaking is the removal of unused code at build time. A bundler can drop an exported function your project never imports, which shrinks the JavaScript a browser must download, parse and execute. It only works reliably with ES modules, and a few common patterns stop it dead.
How it works
- It starts from the entry point. The bundler follows imports from your entry files, keeping the modules you actually reach. Exports nobody imports can be pruned.
- It needs ES module syntax.
importandexportare statically analysable, so the bundler can see which bindings are used. CommonJSrequireis dynamic and opaque, so a fully CommonJS graph is essentially not tree-shakeable. - It pairs with dead-code elimination. Unused local variables and branches no optimisation could prove unused are removed after tree shaking, so a pruned export also lets the bundler drop its internal dead code.
A simple example (the second export is unused and can be dropped):
// utils.js
export const used = () => 42;
export const unusedHelper = () => expensiveCalculation();
// index.js
import { used } from "./utils.js";
console.log(used());
What blocks it
- Side-effecting modules. The concept of a "side effect" is the whole game. If the bundler cannot prove a module is safe to drop (it executes code beyond defining exports), it keeps the file to avoid breaking behaviour, so calling code at the top level of a module kills pruning of that file.
- Non-pure exports. An export wrapped in a call with side effects, for example
export const x = makeThing()wheremakeThingwrites to a global, is kept because removing it would change behaviour. - Dynamic or property access. Bundlers keep live bindings and objects when you use them dynamically, such as accessing
obj[name], because they cannot statically know which keys you need. - A missing
"sideEffects": false. Many published packages are purely exports. If they do not mark themselves side-effect free, the bundler conservatively keeps files it could otherwise drop. You can declare your own side-effect-free files to help.
Enable it fully
- Author ES modules end to end. Keep your source and dependencies in
import/export. Convert or avoid CommonJS code at the edges so the standing graph stays static. - Mark
sideEffectsin package.json. If your package has no side effects, set"sideEffects": false. If only certain files have effects, list them:"sideEffects": ["./src/polyfills.js"]. - Write pure, importable code. Move side effects (global setup, analytics) into explicitly marked files so the rest can be pruned.
- Confirm with your bundler's profiler. Build tools such as Webpack, Rollup and esbuild report output sizes; compare a full bundle against one where you disabled tree shaking or added no side-effect hints, to confirm the tool is actually pruning.
- Verify per-feature. A component library that ships many components benefits most; check that importing one component yields a small chunk, not the whole library. For route-level scale this pairs with code splitting.
Prevention
- Treat tree shaking as opt-in for libraries. Do not assume a dependency is prune-friendly; read its package.json for
sideEffectsand its build output format. - Run a coverage check. A quick DevTools coverage pass on a production bundle shows you whether large unused modules are still landing, which signals a tree-shaking gap rather than a size target miss.