On this page · sections
Code Splitting Strategies
Code splitting breaks the application bundle into smaller chunks loaded on demand — reducing initial parse/compile/execute cost and improving LCP and TTFI.
| Strategy | Split boundary | Load trigger |
|---|---|---|
| Route-level | Page/route components | Navigation |
| Component-level | Heavy widgets (charts, editors) | User action or viewport |
| Vendor split | node_modules | Shared cache chunk |
| Dynamic import | Any module | import() call |
Route-Level Splitting
Definition
Each route becomes its own async chunk — only the landing route loads on first visit.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
export function AppRoutes() {
return (
<Suspense fallback={<RouteSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}Next.js App Router splits automatically per route segment — each page.tsx is a separate chunk. Keep shared layout in parent — don’t duplicate heavy imports in every page.
Prefetch adjacent routes on hover (<Link prefetch>) or after idle. Critical path = landing route only — audit with Coverage panel.
Interview: Route split = one chunk per route. Initial bundle should not include dashboard/analytics code.
Component-Level Dynamic Import
Definition
Split heavy components not needed on first paint — charts, rich text editors, PDF viewers, maps.
import { lazy, Suspense, useState } from 'react';
const ChartPanel = lazy(() => import('./ChartPanel'));
function DashboardShell() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<KpiCards /> {/* lightweight — in main bundle */}
<button onClick={() => setShowChart(true)}>Show chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<ChartPanel />
</Suspense>
)}
</div>
);
}Patterns:
- Interaction-triggered — load on button click
- Viewport-triggered —
IntersectionObserverloads when near viewport - Named exports —
lazy(() => import('./X').then(m => ({ default: m.NamedExport })))
Avoid waterfall: don’t chain import() inside useEffect without parallelizing independent chunks.
Interview: Split at the boundary where user might never need the code. Show skeleton during load.
Vendor Chunk Strategy
Definition
Bundlers (Vite, webpack, Turbopack) extract node_modules into vendor chunks via splitChunks / Rollup manual chunks.
// vite.config.ts — conceptual
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-charts': ['recharts', 'd3'],
},
},
},
},
};Goals:
- Stable vendor hash — app code changes don’t invalidate react chunk cache
- Separate heavy libs — chart library not in main vendor if only analytics route uses it
- Avoid over-splitting — too many small chunks = HTTP overhead (mitigated by H2)
Move route-only deps into route chunks via dynamic import — don’t import chart library in shared layout.
Interview: Vendor split for cache stability. Route-only libs belong in route chunks, not shared vendor.
Measure and Validate Splits
Definition
Verify splits actually help — not just create more requests.
// Webpack/Vite bundle analyzer — inspect chunk sizes
// Chrome Coverage: Ctrl+Shift+P → Coverage → reload
// Lighthouse → unused JavaScript audit
window.addEventListener('load', () => {
const resources = performance.getEntriesByType('resource');
const jsChunks = resources.filter((r) => r.name.endsWith('.js'));
jsChunks.forEach((r) => {
console.log(r.name, (r.transferSize / 1024).toFixed(1) + ' KB');
});
});Targets:
- Initial JS < 200KB gzip for content pages (guideline)
- < 30% unused bytes on first load (Coverage panel)
- Route chunks load < 500ms on 3G after navigation
Compare before/after: TBT, LCP, parse/compile time in Performance panel. Splitting that adds waterfall without reducing initial bytes is wasted complexity.
Interview: Measure initial bundle size and unused bytes. Split should reduce first-load JS, not just chunk count.
Deployment and Caching Policy
Definition
Staff-level split + cache strategy:
main.[hash].js → short cache (app code changes often)
vendor-react.[hash] → long cache (immutable hash)
route-dashboard.[hash] → long cache per routeCache-Control: public, max-age=31536000, immutable (hashed assets)
Cache-Control: no-cache (index.html / SSR shell)Deployment rules:
- Content-hash filenames — safe long cache
- Single vendor chunk for stable deps — avoid hash churn on unrelated changes
- Modulepreload entry + vendor in HTML
- Rollback — old chunks served until CDN TTL if hash changes
- RUM — track chunk load failures (404 on stale chunk after deploy)
Feature flags: load experimental chunks only when flag enabled — don’t bloat main bundle. CDN HTTP/2 multiplexing reduces split penalty but bandwidth is still finite.
Interview: Hashed filenames + immutable cache for chunks. HTML/shell is no-cache. Monitor 404 on stale chunks after deploy.
Senior Interview Summary
Mental model
Initial load → landing route + shared vendor only
Navigation → lazy route chunks on demand
Heavy widgets → dynamic import on interaction/viewport
Cache → stable vendor hash, long immutable TTLPractical rules
- Route split by default in SPA/router frameworks
- Dynamic import for charts, editors, maps
- Keep route-only deps out of shared layout imports
- Validate with Coverage panel — target low unused bytes
- Prefetch likely next route on hover/idle
What to say in interviews
“I’d route-split with React.lazy so the landing page doesn’t download dashboard code. Heavy chart libraries load via dynamic import when the user opens analytics. Vendor chunk separates react from app code for cache stability. I’d verify with Coverage that initial unused JS is under 30% and initial gzip is under 200KB.”
Common traps
- Importing heavy lib in shared layout — defeats route split
- Over-splitting into dozens of tiny chunks
- Splitting without measuring — adds waterfall, no initial savings
- Forgetting Suspense fallback — layout shift on chunk load