Code splitting strategies — route, component, vendor
Three axes: route-level (each page its own chunk), component-level (heavy widgets behind dynamic import), and vendor (long-lived deps in their own chunk for cache reuse). Combine all three; default to route-level first.
Code splitting is how you stop shipping the whole app on every request. Three complementary strategies:
1. Route-level splitting (the biggest win) Each route gets its own chunk. Users on the home page don't download settings code. Frameworks (Next, Remix, React Router) do this for you.
2. Component-level splitting A heavy widget — chart library, markdown editor, emoji picker — behind React.lazy / next/dynamic. The chunk only loads when the user opens the feature. Pair every Suspense boundary with an Error Boundary.
3. Vendor splitting Long-lived third-party deps (React, lodash, charting) in their own chunk. They change rarely, so the user's cached chunk stays valid across deploys. Bundlers do this with splitChunks.cacheGroups (Webpack) automatically in modern presets.
4. Route-prefetch On link hover or idle, prefetch the next likely chunk. Next's <Link> does this; without it, route navigation feels slow.
Antipatterns to avoid:
- Splitting too aggressively — hundreds of tiny chunks, HTTP overhead beats the savings (HTTP/2 mitigates but doesn't eliminate).
- Splitting above-the-fold critical UI — adds a network round trip to first paint.
- Forgetting Error Boundaries — a failed chunk shows the fallback forever.
Measurement. Track initial JS shipped and largest chunk. A 200KB initial budget on mid-tier mobile is a reasonable starting target.
Code
Follow-up questions
- •How does prefetching change the user perception of route changes?
- •What's the difference between dynamic import and React.lazy?
- •When does HTTP/2 multiplexing remove the 'too many chunks' concern?
Common mistakes
- •Lazy-loading the LCP component and tanking initial paint.
- •Splitting per-component without cache strategy — unnecessary churn.
- •Forgetting `priority` on the LCP image when route splitting changes which image is critical.
Performance considerations
- •Each chunk has parsing cost too — splitting helps download but every chunk still has to parse on first use.
Edge cases
- •A chunk fails to load (deploy in flight). Implement retry-with-backoff on dynamic imports.
- •ESM module preloading can be configured with `<link rel='modulepreload'>` for predictably-needed routes.
Real-world examples
- •Next.js App Router auto-splits per route; component-level dynamic import handles heavy widgets explicitly.