Suggest an editImprove this articleRefine the answer for “What is code splitting?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Code splitting** is breaking a JavaScript bundle into smaller chunks that the browser downloads on demand, not all at once. Dynamic `import()` is the mechanism: the bundler creates a separate file at each call site, and the browser fetches it only when that code path runs. **Key point:** route-based splitting with `React.lazy` is the most common pattern. Next.js does it automatically per page.Shown above the full answer for quick recall.Answer (EN)Image**Code splitting** is the practice of breaking one large JavaScript bundle into multiple smaller chunks that the browser downloads only when it actually needs them. ## Theory ### TL;DR - Without splitting, the browser downloads the entire app before rendering anything - Dynamic `import()` tells the bundler to create a separate chunk file at that call site - Route-based splitting is the most common pattern: one chunk per page or route - `webpackPrefetch` lets the browser download chunks during idle time, before the user navigates - Over-splitting creates its own problem: 50 tiny requests can be slower than one medium bundle ### How the Browser Gets the Code When Webpack or Vite processes your app, every static `import` ends up in the same output file. The moment you write a dynamic `import()`, the bundler stops and says: this module gets its own file. At runtime, the browser fetches that file via a new HTTP request the first time that code path executes. ```javascript // Static import - goes into main bundle import { format } from 'date-fns'; // Dynamic import - creates a separate chunk const { Chart } = await import('./Chart.js'); ``` That second line makes the bundler emit something like `chunk-abc123.js` alongside your main bundle. The browser only downloads it when execution reaches that `await import(...)`. ### Route-Based Splitting React Router plus `React.lazy` is the standard pattern in React apps: ```javascript import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); } ``` Each page is now a separate chunk. A user who only visits `/dashboard` never downloads the Settings bundle. ### Component-Level Splitting Route boundaries are not the only place to split. Heavy components like rich text editors, map widgets, or PDF viewers are good candidates too: ```javascript const RichEditor = lazy(() => import('./RichEditor')); // 200kb editor library function PostForm({ showEditor }) { return ( <div> {showEditor && ( <Suspense fallback={<p>Loading editor...</p>}> <RichEditor /> </Suspense> )} </div> ); } ``` The editor loads only when `showEditor` is `true`. If most users never click "write a post," they never pay the cost. ### What the Bundler Does Internally Webpack reads `import()` calls during the build and generates two things: a chunk file and a small runtime snippet that knows how to fetch it. In production you get a hashed filename like `312.abc1d2.js`. You can control the name with a magic comment: ```javascript const Chart = await import( /* webpackChunkName: "analytics-chart" */ './Chart' ); ``` Vite works similarly but uses Rollup's chunking algorithm. Next.js handles route splitting automatically: every file in `app/` or `pages/` becomes its own chunk with no configuration required. ### Prefetch and Preload Loading a chunk only when the user clicks a link means they wait for a network round trip. Webpack supports two hints to avoid that: - `webpackPrefetch: true` - the browser downloads the chunk during idle time, before the user needs it - `webpackPreload: true` - the browser fetches it in parallel with the parent chunk ```javascript // Downloaded in the background when the browser is idle const Analytics = await import( /* webpackPrefetch: true */ './Analytics' ); ``` Prefetch is the right default for most route transitions. Preload is for chunks that are definitely needed alongside the current page. ### Common Mistakes **Splitting too fine-grained.** If you create 50 separate 5kb chunks, the browser makes 50 HTTP/1.1 requests. Even with HTTP/2 multiplexing, the overhead adds up. A practical rule: anything under 30-40kb is probably not worth its own chunk. **No error boundary with Suspense.** If a chunk fails to load (network error, a deploy happening mid-navigation), React throws. Without an error boundary, the whole app crashes. ```jsx // App will crash on chunk load failure <Suspense fallback={<Loading />}> <LazyRoute /> </Suspense> // This handles the failure gracefully <ErrorBoundary fallback={<p>Failed to load. Try refreshing.</p>}> <Suspense fallback={<Loading />}> <LazyRoute /> </Suspense> </ErrorBoundary> ``` **Duplicating shared dependencies.** If `ChartA` and `ChartB` both import `lodash`, and you split both, `lodash` ends up in each chunk file. Webpack's `SplitChunksPlugin` handles this automatically in most configurations, but it is worth verifying with a bundle analysis tool. **Splitting without looking at the bundle first.** Split based on data, not instinct. Tools like `webpack-bundle-analyzer` or Vite's `rollup-plugin-visualizer` show exactly what lives in each chunk and how large it is. ### Real-World Usage - **Next.js** - automatic page-level splitting, no configuration needed - **Vite** - automatic splitting on dynamic imports; `manualChunks` option for vendor extraction - **React Router v6** - combine with `React.lazy` for route-level splitting - **Vue** - `defineAsyncComponent(() => import('./Component.vue'))` works the same way - **Angular** - lazy-loaded modules via the router's `loadChildren` property ### Follow-Up Questions **Q:** What is the difference between `React.lazy` and a plain dynamic import? **A:** `React.lazy` wraps a dynamic import and integrates with Suspense, so React handles the loading state for you. A plain `import()` just returns a Promise and you manage loading state manually. Also, `React.lazy` only works with default exports. **Q:** How does Next.js handle code splitting compared to a standard React app? **A:** Next.js splits at the file level automatically: every page in `app/` or `pages/` is its own chunk. In a plain React setup you add `React.lazy` manually. Next.js also server-renders HTML, so the user sees content before the JS chunk loads at all. **Q:** What happens if the user moves to another route while a chunk is still loading? **A:** React cancels the previous Suspense boundary's pending state and starts loading the new chunk. The old network request continues in the background (browsers do not cancel in-flight fetches), but its module is ignored once it arrives. **Q:** Can you split code by user role, so admins get different chunks than regular users? **A:** Yes. If the user is not an admin, the `import()` call for admin routes simply never executes, so the chunk is never requested. This is a common pattern for role-based feature gating. **Q:** Which performance metric does code splitting most directly improve? **A:** Time to Interactive (TTI) and Largest Contentful Paint (LCP) when the removed code was blocking rendering. The improvement shows up in Lighthouse's "Remove unused JavaScript" audit. In practice, first-paint time noticeably improves when the main bundle drops below roughly 150-200kb gzipped. ## Examples ### Basic Dynamic Import ```javascript // Without splitting: heavyLib loads on every page view // import { processData } from './heavyLib'; // With splitting: heavyLib loads only when the user clicks Export async function handleExport() { const { processData } = await import('./heavyLib'); const result = processData(userData); downloadFile(result); } document.getElementById('export-btn') .addEventListener('click', handleExport); ``` The `processData` library might weigh 150kb. Without splitting, every user pays that cost on page load even if they never click "Export." ### Route Splitting with Error Handling ```javascript import { lazy, Suspense } from 'react'; import { Routes, Route } from 'react-router-dom'; import ErrorBoundary from './ErrorBoundary'; const AdminPanel = lazy(() => import('./pages/AdminPanel')); const UserProfile = lazy(() => import('./pages/UserProfile')); function App() { return ( <ErrorBoundary fallback={<p>Page failed to load. Please refresh.</p>}> <Suspense fallback={<div className="spinner" />}> <Routes> <Route path="/admin" element={<AdminPanel />} /> <Route path="/profile" element={<UserProfile />} /> </Routes> </Suspense> </ErrorBoundary> ); } ``` The `ErrorBoundary` catches chunk load failures. Without it, a deploy that happens while a user is mid-navigation would crash the entire app with no recovery path. ### Prefetch on Hover ```javascript import { lazy, Suspense } from 'react'; const HeavyDashboard = lazy(() => import('./HeavyDashboard')); function NavLink({ to, children }) { // Kicks off the chunk download before the user clicks const prefetch = () => import('./HeavyDashboard'); return ( <a href={to} onMouseEnter={prefetch} // prefetch on hover onFocus={prefetch} // prefetch on keyboard focus > {children} </a> ); } ``` By the time the user's finger lifts off the mouse button, the chunk is likely already cached. I've seen this pattern cut perceived navigation time from 800ms to under 100ms on slower connections.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.