Back to Performance
Performance
medium
mid

How have you implemented code splitting in a real project?

Split by route (lazy-load each page), by component (heavy/below-the-fold/modal components), and by vendor chunks. Use dynamic import() + React.lazy/Suspense, prefetch likely-next chunks, and measure with bundle analysis. Goal: small initial bundle, load the rest on demand.

6 min read·~12 min to think through

Code splitting = breaking the bundle into chunks loaded on demand instead of one big upfront download. I'd describe where I split, how, and how I measured it.

Where to split

1. Route-based (the biggest win, do this first) Each route/page becomes its own chunk — users only download the code for the page they're on.

js
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
// wrap routes in <Suspense fallback={<RouteSkeleton/>}>

2. Component-based Split heavy or rarely-used components even within a route:

  • Below-the-fold sections, modals/dialogs, complex editors, charts.
  • Heavy dependencies — a charting lib, a rich-text editor, a date picker — loaded only when that feature is used.
js
const ChartModal = lazy(() => import("./ChartModal"));

3. Vendor / library splitting Bundler config (splitChunks in webpack, Vite's defaults) separates node_modules so vendor code is cached independently and an app change doesn't bust the vendor chunk.

4. Conditional / on-interaction Dynamically import() a feature only when triggered — e.g. the analytics dashboard code loads when the user opens that tab.

How

  • import() dynamic imports — the bundler automatically creates a chunk at each one.
  • React.lazy + Suspense for components, with a meaningful fallback (skeleton, not a bare spinner).
  • Route-level error boundaries so a chunk that fails to load shows a recoverable error (stale chunks after a deploy are a real failure mode — handle with a reload prompt).

Don't just split — prefetch

Lazy-loading adds a network round-trip when the user navigates. Mitigate:

  • Prefetch on hover/intent — start loading the route chunk when the user hovers the link.
  • Prefetch likely-next chunks during idle time.
  • Frameworks (Next.js) prefetch in-viewport links automatically.

Measure

  • Bundle analyzer (webpack-bundle-analyzer, rollup-plugin-visualizer) — see what's in each chunk, find the bloat.
  • Track initial bundle size in CI with a budget; watch for regressions.
  • Verify in the Network panel that chunks load when expected.
  • Check it actually improved load metrics (LCP/TTI) — don't over-split into hundreds of tiny chunks (request overhead).

The framing

"I split route-first — each page lazy-loaded with React.lazy + Suspense — then split heavy components like modals, charts, and editors, and let the bundler separate vendor chunks. I added hover-prefetching so navigation still feels instant, wrapped routes in error boundaries for failed chunk loads, and tracked initial bundle size in CI with the bundle analyzer. The goal: a small initial download, everything else on demand, without over-splitting."

Follow-up questions

  • Why split by route first?
  • How do you avoid the navigation delay that lazy-loading introduces?
  • What happens when a lazy chunk fails to load (e.g. after a deploy)?
  • How can over-splitting hurt performance?

Common mistakes

  • Splitting without prefetching, making every navigation feel slow.
  • No error boundary for failed chunk loads — white screen after a deploy.
  • Over-splitting into hundreds of tiny chunks, adding request overhead.
  • Bare spinner fallbacks instead of layout-matched skeletons.
  • Never measuring — assuming splitting helped without checking bundle size or load metrics.

Performance considerations

  • Smaller initial bundle = faster TTI/LCP. But each chunk is a request — over-splitting trades parse time for round-trips. Prefetching hides the lazy-load latency. Vendor splitting improves cache hit rates across deploys.

Edge cases

  • Stale chunk references after a new deploy (chunk-load errors).
  • A lazy component needed immediately on first paint (don't split that).
  • SSR — lazy components need framework support to render on the server.
  • Shared code duplicated across chunks if split poorly.

Real-world examples

  • Route-based splitting in a React Router or Next.js app, each page its own chunk.
  • A heavy chart library loaded only when the analytics modal opens.

Senior engineer discussion

Seniors describe a layered strategy (route → component → vendor → on-interaction), pair splitting with prefetching so UX doesn't regress, handle failed/stale chunk loads with error boundaries, and measure with a bundle analyzer + CI size budgets. They warn against over-splitting and tie it back to actual load metrics.

Related questions