FSD Frontend System Design

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.

  • Time17 min
  • DiagramBundle treemap before/after + caching split
  • Chapter5 Β· Performance

Diagram

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.
diagram
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 gzip
diagram
CACHING 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

  1. Measure first with source-map-explorer or rollup-plugin-visualizer β€” optimising a bundle you haven't seen is guessing.
  2. Split by route, then by interaction (modals, editors, charts) using dynamic import().
  3. Replace the heavyweights. A handful of dependency swaps usually beats weeks of code-level tuning.
  4. Enable tree shaking properly: ESM output, "sideEffects": false where true, and named imports β€” import { debounce } from 'lodash-es', never the default import.
  5. Hash filenames and cache immutably, keeping vendor and app code in separate chunks so a feature deploy doesn't invalidate React.
js
// 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

TechniqueWinCost
Route splittingLarge, immediateLoading states per route
Too many chunksβ€”Request overhead, waterfalls
Dependency replacementOften the biggest single winMigration effort
Immutable cachingRepeat visits near-freeRequires hashed filenames
Preloading next routeFeels instantWasted 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-limit gate 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.