Code Splitting
Learn what code splitting is, how React.lazy and dynamic import create smaller chunks, and why route-based splitting is the most common real-world pattern.
Introduction
By default, a bundler like Vite or webpack packages your entire application into one large JavaScript file. As an app grows — more pages, more features, more third-party libraries — that single bundle keeps growing too, and users end up downloading code for pages they may never visit before they can see anything at all.
Code splitting solves this by breaking the bundle into smaller chunks that load only when needed, rather than all at once. You already saw one piece of this in the earlier Lazy Loading lesson — this lesson zooms out to the bigger picture of code splitting as a strategy.
- What code splitting is and the problem it solves.
- How React.lazy() and dynamic import() split code at the component level.
- Route-based code splitting, the most common real-world pattern.
- How to check bundle size in a Vite build.
- Why smaller initial bundles improve page-load performance.
What is Code Splitting?
Code splitting is the practice of dividing your application's JavaScript into multiple smaller files ("chunks") instead of one giant bundle, and loading each chunk only when it is actually needed — typically when the user navigates to a page or opens a feature that requires it.
This is a build-tool-level concept: Vite, webpack, and other modern bundlers can automatically split code at certain boundaries, most commonly wherever your code uses a dynamic import() call instead of a static import at the top of a file.
One Bundle → Many Chunks
Instead of a single app.js, the build produces several smaller files.
Loaded On Demand
A chunk downloads only when the code inside it is actually needed.
Faster Initial Load
The browser downloads, parses, and executes less code before the first render.
Bundler-Driven
Vite/webpack detect dynamic import() calls and automatically create separate chunks.
React.lazy and Dynamic import()
The main React-specific mechanism for code splitting is React.lazy(), combined with JavaScript's dynamic import() syntax. Instead of importing a component at the top of the file (which bundles it into the main chunk), you defer the import until the component is actually about to render.
import Settings from "./Settings";
function App() {
return <Settings />;
}import { lazy, Suspense } from "react";
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<Suspense fallback={<p>Loading settings...</p>}>
<Settings />
</Suspense>
);
}main.js (App shell, does NOT include Settings code)
Settings-a1b2.js (separate chunk, downloaded only when <Settings /> renders)This is exactly the React.lazy() + Suspense pattern from the earlier Lazy Loading lesson. Code splitting is the broader performance strategy; React.lazy is the primary tool React gives you to achieve it at the component level.
Route-Based Code Splitting
The most common and highest-impact place to apply code splitting is at the route level. Each page in your app (Home, Dashboard, Settings, Profile) becomes its own chunk, loaded only when the user actually navigates to that route. Users typically only visit one or two pages per session, so there is little reason to ship every page's code upfront.
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./pages/Home"));
const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));
function App() {
return (
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}With this setup, a first-time visitor landing on the home page only downloads the Home chunk (plus the shared app shell) — Dashboard.js and Settings.js are not fetched until the user actually navigates there.
Checking Bundle Size with Vite
When you build a Vite project for production, it prints a summary of every generated chunk and its size, so you can quickly see whether code splitting is actually working.
npm run builddist/assets/index-4f8a2c.js 142.35 kB │ gzip: 45.11 kB
dist/assets/Dashboard-9d1e6b.js 38.92 kB │ gzip: 12.04 kB
dist/assets/Settings-77bf3a.js 21.60 kB │ gzip: 7.32 kB
dist/assets/Home-1a3c9f.js 9.84 kB │ gzip: 3.15 kBNotice Dashboard, Settings, and Home are separate files instead of being merged into index.js. A visitor to the home page only needs index-*.js and Home-*.js, not the Dashboard or Settings chunks.
Why This Matters for Performance
Every kilobyte of JavaScript the browser downloads has to be fetched, parsed, and executed before the page becomes interactive. A large, un-split bundle delays this for every single visitor, even those who only ever look at one page of your app.
- Smaller initial bundles mean faster download, parse, and execution time on first load.
- This directly improves metrics like Time to Interactive and First Contentful Paint.
- Users on slower connections or lower-end devices benefit the most.
- Code for rarely-visited pages or features never has to be downloaded by users who never reach them.
Common Mistakes
- Splitting so aggressively (dozens of tiny chunks) that the overhead of many small network requests outweighs the benefit.
- Forgetting to wrap lazy-loaded components in a Suspense boundary, which causes an error at render time.
- Code-splitting components that are needed immediately on first load, which just delays showing them with no real benefit.
- Never checking the actual build output, so you cannot tell whether splitting is producing the chunks you expect.
Best Practices
- Start with route-based code splitting — it gives the biggest win for the least effort.
- Use React.lazy() + Suspense for large components that are not needed on initial render (modals, heavy charts, rarely-used settings panels).
- Periodically check your build output to confirm chunks are being created and are reasonably sized.
- Keep an eye on total number of chunks too — extremely fine-grained splitting can add its own overhead.
Frequently Asked Questions
Is code splitting the same thing as lazy loading?
They are closely related. Code splitting is the general strategy of breaking a bundle into chunks; lazy loading (via React.lazy) is the specific React technique for deferring when a chunk is downloaded and rendered.
Do I have to configure Vite manually to enable code splitting?
No. Vite automatically creates a separate chunk for every dynamic import() call — you get code splitting just by using React.lazy(() => import(...)) in your code.
Does code splitting help returning visitors as much as new visitors?
Less so, since browsers cache previously-downloaded chunks. The biggest benefit is for first-time visits and after a new deployment invalidates the cache.
Should every component be code-split?
No. Small, frequently-used components are not worth splitting — the goal is targeting large, infrequently-needed pieces like whole pages or rarely-opened features.
What happens if a lazy-loaded chunk fails to download?
It throws an error during rendering, which you can catch with an Error Boundary (covered in an earlier lesson) to show a fallback UI instead of crashing the app.
Key Takeaways
- Code splitting breaks a single large JS bundle into smaller chunks loaded on demand.
- React.lazy() combined with dynamic import() is the main React-specific mechanism for this.
- Route-based code splitting — one chunk per page — is the most common real-world pattern.
- Vite's build output shows each generated chunk and its size, letting you verify splitting is working.
- Smaller initial bundles mean faster first paint and Time to Interactive, especially on slower connections.
Summary
Code splitting lets you ship only the JavaScript a user actually needs for the page they are on, using React.lazy() and dynamic import() to defer loading the rest. Route-based splitting is the most common way to apply this, and Vite's build output makes it easy to confirm the chunks are being created as expected.
Next, you will move into working with data from the outside world, starting with the browser's built-in Fetch API for loading data into a component.
- You understand what code splitting is and why it matters.
- You can use React.lazy() and dynamic import() to split components.
- You know how to apply route-based code splitting.
- You can check chunk sizes in a Vite production build.