How does code splitting work?
The Short Answer
Code splitting breaks your JavaScript bundle into smaller chunks that are loaded on demand rather than all at once. Instead of shipping one massive file containing your entire app, you split it at logical boundaries (routes, features, heavy components) so users only download the code they actually need for the current page. This dramatically reduces initial load time — especially for large applications where most users only visit a fraction of the available pages.
Why It Matters
Without code splitting, a bundler like webpack or Vite combines all your JavaScript into one or a few large files. A user visiting your homepage downloads code for the admin dashboard, settings page, and every other route — even if they never visit those pages. This wastes bandwidth, increases parse time, and delays interactivity.
The encyclopedia analogy
Without code splitting, visiting a website is like being forced to carry an entire encyclopedia just to read one article. With code splitting, you only pick up the volume you need — and grab another one later if you need it.
Dynamic import() — The Mechanism
Code splitting is powered by dynamic import() — a function that loads a module asynchronously and returns a Promise. Unlike static import statements (which are bundled together), dynamic imports tell the bundler to create a separate chunk that's loaded at runtime only when the import is executed.
// Static import — bundled into the main chunk (always loaded)
import { heavyFunction } from './heavy-module';
// Dynamic import — creates a separate chunk (loaded on demand)
async function handleClick() {
const { heavyFunction } = await import('./heavy-module');
heavyFunction();
}
// The bundler sees import() and creates:
// - main.js (your app code)
// - heavy-module.chunk.js (loaded only when handleClick runs)
The bundler automatically handles chunk creation, naming, and loading. You just use import() where you want a split point, and the tooling does the rest — generating separate files and inserting the loading logic.
Route-Based Splitting in React
The most impactful split point is at the route level — each page becomes its own chunk. React provides React.lazy() for this, which wraps a dynamic import and returns a component that loads its code on first render. Pair it with Suspense to show a fallback while the chunk loads.
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Each route is a separate chunk — loaded only when navigated to
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 (
<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>
);
}
// Result: visiting / only loads home.chunk.js
// Navigating to /admin loads admin.chunk.js on demand
In Next.js, route-based splitting happens automatically — each page in the app/ or pages/ directory becomes its own chunk without any manual configuration. The framework handles lazy loading and prefetching for you.
Component-Level Splitting
Beyond routes, you can split heavy components that aren't needed immediately — modals, charts, rich text editors, or any feature that's triggered by user interaction. This keeps the initial bundle lean and loads expensive code only when the user actually needs it.
import { lazy, Suspense, useState } from 'react';
// Heavy chart library — only loaded when user opens analytics
const AnalyticsChart = lazy(() => import('./components/AnalyticsChart'));
// Heavy markdown editor — only loaded when user clicks "Edit"
const MarkdownEditor = lazy(() => import('./components/MarkdownEditor'));
function ArticlePage({ article }: { article: Article }) {
const [editing, setEditing] = useState(false);
const [showAnalytics, setShowAnalytics] = useState(false);
return (
<div>
<h1>{article.title}</h1>
{editing ? (
<Suspense fallback={<Spinner />}>
<MarkdownEditor content={article.body} />
</Suspense>
) : (
<article>{article.body}</article>
)}
<button onClick={() => setShowAnalytics(true)}>View Analytics</button>
{showAnalytics && (
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart articleId={article.id} />
</Suspense>
)}
</div>
);
}
Prefetching and Preloading
Code splitting introduces a tradeoff: smaller initial load, but a delay when navigating to a new route or opening a lazy component. Prefetching mitigates this by loading chunks in the background before the user needs them — on hover, on idle, or when a link enters the viewport.
// Prefetch on hover — load the chunk before the user clicks
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function NavLink() {
const prefetchAdmin = () => {
// Triggers the chunk download without rendering
import('./pages/AdminPanel');
};
return (
<a
href="/admin"
onMouseEnter={prefetchAdmin}
onFocus={prefetchAdmin}
>
Admin
</a>
);
}
// Prefetch on idle — load non-critical chunks when browser is idle
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
import('./pages/Settings');
import('./pages/AdminPanel');
});
}
Next.js prefetches automatically
Next.js prefetches linked routes when <Link> components enter the viewport. You get the benefits of code splitting without the navigation delay — chunks are already loaded by the time the user clicks.
What to Split
Good candidates for splitting
- ✅Routes / pages (biggest impact, easiest to implement)
- ✅Heavy third-party libraries (chart libs, editors, PDF viewers)
- ✅Features behind user interaction (modals, drawers, tabs)
- ✅Admin-only features (most users never see them)
- ✅Below-the-fold content (not visible on initial viewport)
Don't split
- ❌Tiny components (overhead of a network request outweighs the savings)
- ❌Components needed for initial render (adds delay to first paint)
- ❌Shared utilities used everywhere (they'd be loaded immediately anyway)
- ❌Critical-path UI (header, navigation, layout shell)
Why Interviewers Ask This
This question tests your understanding of frontend performance optimization and build tooling. Interviewers want to see that you know how dynamic import() creates split points, can identify good candidates for splitting, understand the tradeoff (smaller initial load vs navigation delay), know how to mitigate that tradeoff with prefetching, and have practical experience with React.lazy/Suspense or framework-level splitting. It shows you think about user experience beyond just writing correct code.
Quick Revision Cheat Sheet
Mechanism: Dynamic import() tells bundler to create a separate chunk
React API: React.lazy(() => import('./Component')) + Suspense fallback
Best split point: Routes — each page as its own chunk (biggest impact)
Tradeoff: Smaller initial load, but delay on first navigation to new chunk
Mitigation: Prefetch on hover/idle/viewport entry
Next.js: Automatic per-page splitting + Link prefetching built-in