Code Splitting and Lazy Loading
Why Code Split?
A monolithic bundle means every user downloads code for every route — even routes they never visit. Code splitting loads JavaScript on demand.
Real-world impact: E-commerce site reduced initial bundle from 1.2MB to 280KB (77% reduction), cutting Time to Interactive from 3.1s to 0.9s.
React.lazy + Suspense
import { lazy, Suspense } from 'react';
// Lazy load: creates a new bundle chunk
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Route-Based Splitting (Highest Impact)
Split at the route level — users only download code for pages they visit:
const routes = [
{ path: '/', component: lazy(() => import('./pages/Home')) },
{ path: '/products', component: lazy(() => import('./pages/Products')) },
{ path: '/checkout', component: lazy(() => import('./pages/Checkout')) },
{ path: '/admin', component: lazy(() => import('./pages/Admin')) },
];
function Router() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
{routes.map(({ path, component: Component }) => (
<Route key={path} path={path} element={<Component />} />
))}
</Routes>
</Suspense>
);
}
Component-Based Splitting
Split heavy conditional components (modals, charts, editors):
// Only load when modal is actually opened
const HeavyModal = lazy(() => import('./HeavyModal'));
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
{open && (
<Suspense fallback={<ModalSkeleton />}>
<HeavyModal onClose={() => setOpen(false)} />
</Suspense>
)}
</>
);
}
Prefetching — Load Before User Clicks
// Prefetch on hover for instant navigation feel
function NavLink({ to, label }) {
const prefetch = () => import(`./pages/${to}`); // Trigger download
return (
<Link
to={to}
onMouseEnter={prefetch} // Start downloading on hover
onFocus={prefetch} // Keyboard nav too
>
{label}
</Link>
);
}
Named Exports with React.lazy
React.lazy only works with default exports. Create an intermediate module for named exports:
// ✅ Option 1: Re-export as default
// SomeComponent.lazy.js
export { SomeComponent as default } from './SomeComponent';
const Lazy = lazy(() => import('./SomeComponent.lazy'));
// ✅ Option 2: Inline re-export
const Lazy = lazy(() =>
import('./SomeComponent').then(m => ({ default: m.SomeComponent }))
);
Error Boundary + Suspense
Always wrap lazy components in an error boundary to handle network failures:
<ErrorBoundary fallback={<p>Failed to load. <button onClick={retry}>Retry</button></p>}>
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
Split Threshold
Below ~30KB, the HTTP request overhead often outweighs the bundle savings. Aim for 5–15 total chunks for an optimal balance.
Library Splitting
Separate third-party libraries from app code — libraries change infrequently so browsers can cache them longer:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
Content from Frontend-Master-Prep-Series — 03-react