Difficulty: Advanced
Code splitting is the practice of breaking your application's JavaScript bundle into smaller chunks that load on demand. Without code splitting, users download the entire app's code upfront - even for pages they never visit. This increases the initial load time and wastes bandwidth. Code splitting ensures users only download the code they need, when they need it.
React.lazy and dynamic import() are the primary tools for code splitting in React. React.lazy takes a function that calls import() and returns a component that loads on demand. The bundler (Vite, webpack) sees the dynamic import and automatically creates a separate chunk for that module and its dependencies. When the component first renders, the chunk downloads, and Suspense shows a fallback during the download.
Route-based splitting is the most impactful strategy because routes are natural loading boundaries. Users expect a brief loading moment when navigating between pages. By lazy-loading each route component, the initial bundle contains only the layout, navigation, and current page. Other pages load on demand. This can reduce the initial bundle by 50-80% in large applications.
Component-based splitting is useful for heavy features that not all users interact with: complex modals, rich text editors, chart libraries, or admin panels. Instead of including chart.js in the main bundle (it's 200KB+), lazy-load the chart component only when the user navigates to the analytics page.
Bundle analysis tools like rollup-plugin-visualizer (for Vite) or webpack-bundle-analyzer show you exactly what is in each chunk and how large it is. Run the analysis, identify the largest chunks, and split them. Common culprits are large libraries (moment.js, lodash, chart libraries) that should be dynamically imported or replaced with lighter alternatives.
import React, { Suspense, lazy } from 'react';
import { Routes, Route } from 'react-router-dom';
import Layout from './Layout'; // Always loaded
// Each route is a separate chunk
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<Layout>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</Layout>
);
}
Layout loads immediately. When the user navigates to /dashboard, only then does Dashboard.js download. Users who never visit /admin never download its code.
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const ChartDashboard = lazy(() => import('./components/ChartDashboard'));
function ArticleEditor() {
const [showPreview, setShowPreview] = React.useState(false);
return (
<div>
<Suspense fallback={<p>Loading editor...</p>}>
<RichTextEditor />
</Suspense>
<button onClick={() => setShowPreview(true)}>Preview</button>
{showPreview && (
<Suspense fallback={<p>Loading charts...</p>}>
<ChartDashboard />
</Suspense>
)}
</div>
);
}
The chart library (often 200KB+) is not in the initial bundle. It downloads only when the user clicks Preview, which many users may never do.
const Dashboard = lazy(() => import('./pages/Dashboard'));
function NavLink({ to, children }) {
const prefetch = () => {
// Trigger the dynamic import on hover
if (to === '/dashboard') {
import('./pages/Dashboard');
}
};
return (
<Link
to={to}
onMouseEnter={prefetch}
onFocus={prefetch}
>
{children}
</Link>
);
}
// When user hovers over the link, the chunk starts downloading.
// By the time they click, it's often already cached.
Calling import() on hover starts the download. The browser caches the module. When React.lazy renders the component on navigation, the chunk is already available - no loading spinner.
React.lazy, dynamic import, route-based splitting, bundle analysis, prefetching