Part III β The Speed Β· Lesson 5.7
Build Optimization
In one lineShip only the code this route needs, compress it, and let the browser cache it forever.
Where you've seen itA login page that downloads the charting library, the admin panel, and every locale file before it can show two input fields.
Diagram
BUNDLE TREEMAP β where 480KB actually went
BEFORE AFTER
ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
β moment.js + all locales β β react-dom 38KB β
β 180KB β ββββββββββββββββββββ€
ββββββββββββββββ¬ββββββββββββββ€ β app 44KB β
β lodash (full)β react-dom β ββββββββββ¬ββββββββββ€
β 71KB β 38KB β βdate-fnsβ router β
ββββββββββββββββΌββββββββββββββ€ β 6KB β 9KB β
β admin panel β charts β ββββββββββ΄ββββββββββ
β 92KB β 55KB β 97KB initial
ββββββββββββββββ΄ββββββββββββββ€ admin + charts split
β app code 44KB β to lazy routes
ββββββββββββββββββββββββββββββ
480KB β 97KB initial. Nothing was rewritten.THE FOUR MOVES, IN ORDER OF PAYOFF
1. SPLIT route-level lazy imports biggest, easiest win
ββ admin panel β its own chunk
ββ charts β loaded on demand
2. REPLACE moment β date-fns/dayjs 180KB β 6KB
lodash β lodash-es + named 71KB β 4KB
heavy UI kit β the 3 parts you use
3. SHAKE ESM + sideEffects:false drops unused exports
(CommonJS imports can't be tree-shaken)
4. COMPRESS Brotli at build time ~15% under gzipCACHING SPLIT β hash the content, cache forever
vendor.a3f9c1.js Cache-Control: max-age=31536000, immutable
app.7b2e04.js changes every deploy, new hash β new URL
index.html Cache-Control: no-cache β always revalidated
Filename hashing means you never invalidate; you just stop
referencing the old file.How it works
- Measure first with
source-map-explorerorrollup-plugin-visualizerβ optimising a bundle you haven't seen is guessing. - Split by route, then by interaction (modals, editors, charts) using dynamic
import(). - Replace the heavyweights. A handful of dependency swaps usually beats weeks of code-level tuning.
- Enable tree shaking properly: ESM output,
"sideEffects": falsewhere true, and named imports βimport { debounce } from 'lodash-es', never the default import. - Hash filenames and cache immutably, keeping vendor and app code in separate chunks so a feature deploy doesn't invalidate React.
// route-level split β the single highest-value change
const Admin = lazy(() => import('./routes/Admin'));
// interaction-level split β load the editor when it's opened
const openEditor = async () => {
const { Editor } = await import('./Editor');
mount(Editor);
};Trade-offs
| Technique | Win | Cost |
|---|---|---|
| Route splitting | Large, immediate | Loading states per route |
| Too many chunks | β | Request overhead, waterfalls |
| Dependency replacement | Often the biggest single win | Migration effort |
| Immutable caching | Repeat visits near-free | Requires hashed filenames |
| Preloading next route | Feels instant | Wasted bytes if unused |
Don't over-split. Twenty tiny chunks on a slow connection can be slower than three sensible ones β and on HTTP/1.1 it's much worse (see 1.4).
Interview angle
"Your bundle is 500KB. Where do you start?"
- Open the treemap first β in most apps two or three dependencies own half the weight.
- Route-split, replace the heavyweights, verify tree shaking is actually happening (named ESM imports,
sideEffects). - Lock it in with a
size-limitgate in CI (4.5), or it grows straight back next quarter.
Recap
- Measure the treemap, split by route, replace heavy dependencies, compress.
- Tree shaking needs ESM and honest
sideEffectsβ CommonJS silently defeats it. - Hashed filenames plus immutable caching make repeat visits nearly free.