Bundle size: why tree shaking fails
React chapter 146 · Web Perf · ch.7 · tree shaking, ESM vs CommonJS, namespaces, duplicate libraries · source: Nadia Makarevich, Web Performance Fundamentals, Chapter 7 (bundle size, tree shaking, non-tree-shakable libraries, transitive dependencies)
The problem
The app uses one icon, one MUI component, two lodash helpers and formats one date — yet the vendor chunk is megabytes. A namespace object (Ui = { Icons }), a CommonJS library and three date libraries each defeat tree shaking in a different way.
The pattern that fixes it
Open the bundle analyzer, click the biggest block, find the import that pulled it in. Import exactly what you use from ES-module entry points, drop non-tree-shakable libraries or use their per-function paths, keep one library per job, and lazy-load heavy page-only code.
In plain words
Your bundler copies only the code you actually use into the files users download — “tree shaking”. But it can only prove what is unused when imports are simple and static. A few common habits make it give up and include the whole library.
Packing for a trip by reading your itinerary. If the itinerary says “I might need anything from the garage”, the whole garage goes in the suitcase.
Terms
- tree shaking
- Removing exports nothing imports. Works on static import/export (ES modules).
- ESM vs CommonJS
- export const x is analysable at build time. module.exports = {…} is just an object at runtime — usually included whole. Check with npx is-esm <pkg>.
- bundle analyzer
- A treemap of what is in each chunk: rollup-plugin-visualizer (Vite), webpack-bundle-analyzer, @next/bundle-analyzer.
Going deeper
Bytes cost twice
A big bundle costs download time (bad on slow networks, chapter 143) and then parse + compile + evaluate time on the main thread (bad on slow CPUs, it delays LCP in CSR and blocks hydration in SSR). Gzip/Brotli shrink the download, not the parse: the phone still evaluates every minified byte.
Why the namespace object breaks it
import * as Icons alone is fine — the bundler sees Icons.Star and keeps only Star. But export const Ui = { Icons } turns the namespace into a runtime value that could be read with any key (Ui.Icons[name]). The bundler must keep every export. The same happens with re-export barrels that have side effects, or "sideEffects" missing from a library’s package.json.
Transitive dependencies and duplicates
A package you never imported can be in the bundle because a dependency of a dependency needs it — sometimes in a second version. npm why <pkg> shows the chain. Duplicates (moment + luxon + date-fns, two copies of a library at different versions) are common in big multi-team apps; dedupe with one library per job and package-manager overrides/resolutions.
Guard it in CI
Bundle size regressions arrive one innocent import at a time. Add a size budget (size-limit, bundlewatch, or the framework’s build output) to CI so the PR that adds 400 KB fails with a clear diff.
Keep learning
- Previous: INP: the three parts of a click
- Next: Put the LCP element on the critical path
- Related: lazy + preload
- Related: Request waterfalls
- Related: Yield to main
- Related: CSR blank vs SSR
- Related: React Compiler
- Related: Where LCP time goes
More courses
- Jobs, linked to lessons — Browse jobs where every requirement links to the chapter that teaches it. See how ready you are, how long the gap takes to close, and which listings look stale.
- Match a job post to courses — Paste a job post. Each skill is matched to a track here and put in learning order, foundations first.
- Database in depth — How a query becomes work. Indexes, isolation, locks, and the plans that explain why a page is slow — not an ORM tour.
- System Design — Eight interview case studies, from a flash sale and Tatkal to a live final and a chat model, plus JWT in separate chapters.
- Microservices — Microservice patterns one diagram at a time: how to split a monolith, how services talk, and how data stays consistent without distributed transactions.
- Kafka — A beginner-to-senior path through event logs, reliable delivery, production operations, and disaster recovery.
- Frontend system design — How a UI is sliced when it is a product, not a page: ownership of state, cache, routes, and what happens when the network lies.
- CSS in depth — See the box, then the page. Visual chapters from the box model through Flexbox surprises, Grid, selectors, and units, then backgrounds, shapes, and visual effects, each with a playground.
- Accessibility — HTML labs first, then a full pass of WAI-ARIA 1.3: naming, every state, widget and structure roles, live regions, ariaNotify, and relationships.
- Docker — What an image is, why layers cache, and how containers find each other on a network.
- Redis — Fifteen Redis patterns, one picture each: cache, session, lock, counter, rate limiter, queue, and when Redis is the wrong tool.