Skip to main content

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-Series03-react