PatternsCode Splitting Strategies
Route-Based Splitting
Lazy load routes for the biggest performance impact
Route-Based Splitting
The easiest and most impactful code splitting strategy. Split your app by routes - users only download the page they visit.
🎯 The Impact
Without Route Splitting:
User visits / → Downloads EVERYTHING (2.5 MB)
User visits /about → Already has it (but wasted bandwidth)
User visits /dashboard → Already has it (but wasted bandwidth)
With Route Splitting:
User visits / → Downloads 150 KB ✅
User visits /about → Downloads 80 KB more
User visits /dashboard → Downloads 200 KB more
Total downloaded: 430 KB instead of 2.5 MB (83% savings!)⚛️ React Router + Lazy
Basic Setup
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load each route
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}With Loading States
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner" />
<p>Loading page...</p>
</div>
);
}
// Or skeleton loader
function PageLoader() {
return (
<div className="skeleton-page">
<div className="skeleton-header" />
<div className="skeleton-content" />
<div className="skeleton-sidebar" />
</div>
);
}With Error Boundary
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<BrowserRouter>
<ErrorBoundary
fallback={<ErrorPage />}
onError={(error) => console.error('Route load error:', error)}
>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}
function ErrorPage() {
return (
<div>
<h1>Failed to load page</h1>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}🔥 Next.js (Automatic Route Splitting)
Next.js automatically splits by routes - zero config needed!
// pages/index.tsx → Separate chunk
export default function Home() {
return <div>Home</div>;
}
// pages/about.tsx → Separate chunk
export default function About() {
return <div>About</div>;
}
// pages/dashboard.tsx → Separate chunk
export default function Dashboard() {
return <div>Dashboard</div>;
}
// Each page is a separate bundle automatically! ✅App Router (Next.js 13+)
// app/page.tsx → Separate chunk
export default function Home() {
return <div>Home</div>;
}
// app/about/page.tsx → Separate chunk
export default function About() {
return <div>About</div>;
}
// app/dashboard/page.tsx → Separate chunk
export default function Dashboard() {
return <div>Dashboard</div>;
}🎨 Advanced Patterns
Nested Route Splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
// Nested routes also split
const DashboardLayout = lazy(() => import('./layouts/DashboardLayout'));
const Analytics = lazy(() => import('./pages/dashboard/Analytics'));
const Reports = lazy(() => import('./pages/dashboard/Reports'));
const Settings = lazy(() => import('./pages/dashboard/Settings'));
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={
<Suspense fallback={<PageLoader />}>
<DashboardLayout />
</Suspense>
}>
<Route index element={
<Suspense fallback={<Spinner />}>
<Analytics />
</Suspense>
} />
<Route path="reports" element={
<Suspense fallback={<Spinner />}>
<Reports />
</Suspense>
} />
<Route path="settings" element={
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>
} />
</Route>
</Routes>
);
}Named Chunks
// Name your chunks for better debugging
const Dashboard = lazy(() =>
import(
/* webpackChunkName: "dashboard" */
'./pages/Dashboard'
)
);
const Analytics = lazy(() =>
import(
/* webpackChunkName: "analytics" */
'./pages/Analytics'
)
);
// Results in:
// dashboard.chunk.js
// analytics.chunk.jsRetry on Failure
function lazyWithRetry(importFn: () => Promise<any>, retries = 3) {
return lazy(async () => {
for (let i = 0; i < retries; i++) {
try {
return await importFn();
} catch (error) {
if (i === retries - 1) {
throw error;
}
// Wait before retry (exponential backoff)
await new Promise(resolve =>
setTimeout(resolve, 1000 * Math.pow(2, i))
);
}
}
throw new Error('Failed to load module');
});
}
// Usage
const Dashboard = lazyWithRetry(() => import('./pages/Dashboard'));🚀 Preloading Routes
Hover Preload
import { useState } from 'react';
function Navigation() {
const [hoveredRoute, setHoveredRoute] = useState<string | null>(null);
const prefetchRoute = (route: string) => {
// Preload the route component
switch (route) {
case '/dashboard':
import('./pages/Dashboard');
break;
case '/settings':
import('./pages/Settings');
break;
}
};
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={() => {
setHoveredRoute('/dashboard');
prefetchRoute('/dashboard');
}}
>
Dashboard
</Link>
<Link
to="/settings"
onMouseEnter={() => {
setHoveredRoute('/settings');
prefetchRoute('/settings');
}}
>
Settings
</Link>
</nav>
);
}Visible Link Preload
import { useInView } from 'react-intersection-observer';
function PrefetchLink({ to, children }: { to: string; children: React.ReactNode }) {
const { ref, inView } = useInView({ triggerOnce: true });
useEffect(() => {
if (inView) {
// Preload when link becomes visible
import(`./pages/${to}`);
}
}, [inView, to]);
return (
<Link ref={ref} to={to}>
{children}
</Link>
);
}
// Usage
<PrefetchLink to="/dashboard">Dashboard</PrefetchLink>Idle Preload
function App() {
useEffect(() => {
// Preload routes during idle time
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./pages/Dashboard');
import('./pages/Settings');
}, { timeout: 2000 });
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => {
import('./pages/Dashboard');
import('./pages/Settings');
}, 1000);
}
}, []);
return <Routes>...</Routes>;
}📊 Measuring Impact
// Track chunk load time
const Dashboard = lazy(async () => {
const start = performance.now();
const module = await import('./pages/Dashboard');
const loadTime = performance.now() - start;
console.log(`Dashboard loaded in ${loadTime}ms`);
// Send to analytics
analytics.track('chunk_loaded', {
chunk: 'dashboard',
loadTime
});
return module;
});🎯 Best Practices
1. Group Related Routes
// ❌ BAD: Too granular
const DashboardHome = lazy(() => import('./pages/DashboardHome'));
const DashboardView1 = lazy(() => import('./pages/DashboardView1'));
const DashboardView2 = lazy(() => import('./pages/DashboardView2'));
// ✅ GOOD: Group related pages
const Dashboard = lazy(() => import('./pages/Dashboard'));
// Dashboard internally handles sub-routes2. Always Provide Fallback
// ❌ BAD: No fallback
<Suspense>
<Route path="/dashboard" element={<Dashboard />} />
</Suspense>
// ✅ GOOD: Meaningful fallback
<Suspense fallback={<DashboardSkeleton />}>
<Route path="/dashboard" element={<Dashboard />} />
</Suspense>3. Monitor Bundle Sizes
# Check route chunk sizes
npm run build
ls -lh dist/assets/*.js
# Target: < 100 KB per route (gzipped)📚 Key Takeaways
- Biggest impact - Route splitting is #1 optimization
- Zero config in Next.js - Automatic by default
- Always use Suspense - Provide loading states
- Preload critical routes - On hover or visible
- Name your chunks - Better debugging
- < 100 KB per route - Target chunk size
- Monitor over time - Bundle size grows
Route-based splitting typically gives you 70-80% bundle size reduction with minimal effort. Start here! 🚀