Tree shaking is what a bundler does when it looks at every export in your codebase, traces which ones are actually imported anywhere, and deletes the rest before shipping the final bundle. The name comes from the mental image of shaking a tree so the dead leaves, the code nothing depends on, fall away. It sounds automatic, and often it is, but it only works under specific conditions. The moment those conditions break — a CommonJS module in the chain, a barrel file, a missing package.json field — dead code stops falling and quietly rides along in your bundle instead.
What changed in 2026
- Rollup and esbuild-based tooling made tree shaking closer to default-on. Vite and similar modern build tools now tree shake without much manual configuration, where older webpack setups often needed explicit tuning.
- Bundle analyzers became a common CI step, catching size regressions automatically rather than relying on someone noticing the bundle got bigger.
- More npm packages ship proper sideEffects metadata, since bundle size increasingly affects page performance scores that feed into search visibility, giving package authors a direct incentive to mark their code side-effect free.
- Barrel file costs got wider recognition. More style guides and linters now flag deep barrel re-exports as a real performance risk, not just a style preference.
How tree shaking actually works
// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// main.js
import { add } from "./math.js";
console.log(add(2, 3));
multiply is never imported, so a tree-shaking bundler can prove it is unreachable and drop it from the final bundle entirely. This only works because import and export statements are static — the bundler can see the whole dependency graph without executing any code.
Why CommonJS blocks it
// CommonJS - opaque to static analysis
const math = require("./math.js");
console.log(math.add(2, 3));
A bundler cannot safely prove which properties of math actually get used, since require is a normal function call and module.exports is just an object that code elsewhere could mutate. Tree shaking is largely limited to the ES module parts of your dependency graph.
The barrel file problem
A barrel file re-exports many modules through one entry point. Importing a single named export from it can still pull in everything the barrel touches, because evaluating the barrel means evaluating every file it re-exports from, unless side-effect metadata or deeper bundler analysis proves the rest is safe to skip.
// icons/index.ts (barrel file)
export * from "./home-icon";
export * from "./search-icon";
export * from "./settings-icon";
// ...200 more icons
// consuming file
import { HomeIcon } from "./icons";
Even though only HomeIcon is used, some bundler configurations will still evaluate or include the other icon modules. Importing directly from the specific file instead of the barrel avoids the problem entirely.
What helps and what hurts
| Practice |
Effect on tree shaking |
| ES module import/export syntax |
Enables it |
| CommonJS require/module.exports |
Mostly blocks it |
| "sideEffects": false in package.json |
Helps the bundler drop unused files confidently |
| Deep barrel file re-exports |
Often hurts, can pull in far more than needed |
| Named exports instead of one default export object |
Helps, each export is tracked individually |
| Top-level code with side effects, like logging on import |
Hurts, the bundler cannot safely remove it |
How to verify it is actually working
Do not assume tree shaking is happening just because the code is written with import/export. Run a source-map based bundle analyzer to see what actually shipped, remove an export you believe is unused and confirm the bundle shrinks, and track bundle size in CI over time so a regression shows up immediately instead of months later.
FAQ
Does tree shaking work automatically with any bundler?
Most modern bundlers support it, but only for ES module code and only when they can prove a piece of code is unused and safe to remove. It does not work regardless of how the code is written.
Why is my unused code still in the bundle?
The most common causes are a CommonJS module somewhere in the dependency chain, a barrel file re-exporting more than you use, or a missing sideEffects field that forces the bundler to keep files it cannot prove are safe to drop.
Is tree shaking the same as minification?
No. Minification shrinks the code that remains, by shortening names and removing whitespace. Tree shaking removes entire pieces of code that are never used, before minification even runs.
Does tree shaking work with dynamic imports?
Yes, code loaded through a dynamic import is analyzed the same way. Dynamic import is more often used for a related but different goal — see what is code splitting in 2026 for how it defers loading code rather than removing it.
Where to go next