Performance
medium
mid
Tree shaking and bundling — how do they work?
Tree shaking is dead-code elimination over ES modules. Static `import`/`export` syntax lets bundlers analyze the dependency graph and drop exports nothing imports. Side effects (or `sideEffects: false`) decide what's safely removable.
6 min read·~12 min to think through
Bundling packs your modules + dependencies into one or more files the browser can load efficiently (fewer HTTP requests, shared code split into chunks).
Tree shaking is dead-code elimination across modules: if a module exports A, B, C and only A is imported anywhere in the graph, B and C never reach the bundle.
It works because ESM is statically analyzable:
import { x } from "./m"is resolvable at parse time — no runtime branches, norequire()returning different shapes.- The bundler builds a graph of what's exported vs what's actually imported and drops the rest.
What kills tree shaking:
- CommonJS (
require/module.exports) — dynamic by design; bundlers can't reason about which exports are used. - Side effects on import —
import "./styles.css"registers global CSS; even if no symbol is used, the import must run. Mark side-effect-free packages with"sideEffects": falsein theirpackage.json. - Re-exports through barrel files —
export * from "./big-file"can pull large surfaces unless re-exports are themselves marked as ESM with side-effect annotations. - **
/#__PURE__/annotations missing** — toplevel function calls that return values are assumed to have side effects; bundlers need a hint to drop them.
Bundler flavors:
- Webpack — workhorse; tree shaking via
UglifyJS/TerserwithsideEffectsfield. - Rollup — pioneered ES-module tree shaking; great for libraries.
- esbuild / SWC — fast, used by Vite under the hood.
- Vite — dev uses native ESM (no bundling); prod uses Rollup.
Code
Follow-up questions
- •Why doesn't tree shaking work on CommonJS imports?
- •What does `"sideEffects": false` in package.json actually do?
- •How does Vite's dev server avoid bundling, and why is that fast?
Common mistakes
- •Importing a default export of a CJS package and assuming tree shaking works.
- •A barrel file (`index.ts` re-exporting everything) defeats granular imports unless side-effect hints are right.
- •Forgetting that CSS imports always have side effects — tree shaking won't strip styles.
Performance considerations
- •Tree shaking primarily reduces *bundle size* (LCP, parse time). It doesn't help runtime CPU.
Edge cases
- •Class methods can't be tree-shaken individually — the class is one unit.
- •TypeScript decorators / metadata may force a method to be retained for runtime reflection.
Real-world examples
- •lodash-es is tree-shakable; importing `{ debounce }` ships ~2KB instead of lodash's ~70KB.
Senior engineer discussion
Senior signal: discuss module-graph analysis, why mode='production' enables Terser, the role of `__PURE__` annotations, and how bundlers compose with route-level code splitting.
Related questions
Performance
Medium
hot
8 min