A common pattern I have seen involves applying React.lazy to nearly every component in an application, assuming more splitting automatically means better performance, without actually measuring whether this produced a genuine improvement in real loading metrics or simply introduced more loading states without corresponding benefit.
What Code Splitting Actually Solves
Without code splitting, your entire application’s JavaScript bundles into a single file that the browser must download before rendering anything. For larger applications, this single bundle can become large enough that initial load time suffers meaningfully, since users must wait for code related to features they may not even use on their current page before seeing anything render.
Code splitting divides this single bundle into smaller chunks loaded on demand, meaning the initial page load only requires downloading the code genuinely needed for that specific initial view, with other code loading later when actually needed.
Measuring Whether Splitting Actually Helps Your Specific Application
This is worth establishing directly before discussing implementation, since code splitting’s benefit depends genuinely on your application’s actual bundle size and structure. For a genuinely small application, splitting adds complexity (loading states, additional network requests) without meaningful benefit, since the entire bundle may already be small enough to load quickly without splitting.
Using your browser’s network tab, or a bundle analyzer tool, to actually measure your current bundle size and loading time provides the baseline needed to evaluate whether splitting would produce a measurable improvement, rather than applying it reflexively based on the general principle that splitting is good practice regardless of your specific application’s actual characteristics.
Implementing Route-Based Splitting: The Highest-Value Starting Point
In my own testing across several applications, splitting at route boundaries — loading each major page or route’s code only when a user actually navigates to it — consistently produced the most measurable initial load time improvement relative to implementation effort, compared to more granular component-level splitting.
const Dashboard = React.lazy(() => import('./Dashboard'));
const Settings = React.lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
This pattern means a user landing on your application’s homepage does not need to download the code for the Settings page, which only loads when they actually navigate there, measurably reducing the initial bundle size for the common case of users who only visit a subset of your application’s total routes during any given session.
When Component-Level Splitting Genuinely Helps
Beyond route boundaries, splitting specific large, conditionally-rendered components — a complex modal that only appears after a specific user action, a heavy data visualization library only needed on one specific page section — can provide additional measurable benefit, particularly for genuinely large dependencies that are not needed for the majority of a typical user session.
I measured this directly for an application including a large charting library used only within one specific, infrequently-accessed report view. Splitting this specific component out, rather than including it in the main bundle, reduced the main bundle size measurably, confirmed through bundle analyzer comparison before and after this specific change.
When Component-Level Splitting Adds Unnecessary Complexity
For smaller components, or components that render immediately on most page loads regardless of route, splitting adds the overhead of a loading state and additional network request without genuine corresponding benefit, since the component would have loaded as part of the initial bundle regardless, just integrated into a single request rather than split into a separate one with its own loading overhead.
Over-splitting can actually harm performance in some cases by introducing excessive numbers of small network requests, each carrying some inherent request overhead, rather than the http/2 or http/3 connection reuse benefits offsetting this for genuinely many small chunks.
Designing Appropriate Loading States
The Suspense fallback shown during a lazy-loaded component’s loading period genuinely affects perceived performance, beyond just the actual measured loading time. A jarring, unstyled loading state, or one that causes significant layout shift once the actual content loads, can make an objectively fast load feel poor, while a well-designed loading skeleton that closely matches the eventual content’s layout tends to feel smoother even at similar actual loading durations.
This is worth deliberate design attention rather than defaulting to a minimal generic spinner for every Suspense boundary, particularly for boundaries that users encounter frequently.
Combining Code Splitting With Preloading for Likely Navigation
For routes or components that a user is likely to navigate to soon — based on their current page or a hover state on a navigation link — preloading the relevant chunk slightly ahead of the actual navigation can mean the code is already loaded or loading by the time the user actually navigates, reducing the perceived wait compared to only beginning the load at the exact moment of navigation.
This adds implementation complexity and is worth reserving for genuinely high-value navigation paths (a clearly likely next step in a common user flow) rather than applying preloading universally across every possible navigation target, which would partially undermine the initial bundle size benefit that splitting was intended to provide.
A Quick Reference for Splitting Decisions
| Situation | Splitting Recommendation |
|---|---|
| Distinct application routes/pages | Strong candidate, generally high value |
| Large, conditionally-rendered components (modals, heavy libraries) | Good candidate if genuinely large and not always needed |
| Small components rendering on most page loads | Skip — overhead likely exceeds benefit |
| Already small overall bundle | Verify actual benefit before adding complexity |
What Measuring Actually Revealed
Going back through an application where component-level splitting had been applied broadly without measurement, removing splitting from the smaller, frequently-rendered components while keeping it specifically for route boundaries and the genuinely large conditional components actually improved measured performance slightly, by reducing the number of small, overhead-carrying network requests that the over-aggressive splitting had introduced, confirming that more splitting is not automatically better without verifying the specific boundary actually justifies the added complexity.
Are you trying to improve a specific application’s load time, or deciding how aggressively to apply code splitting? Describe your situation and I can help you think through where splitting would likely provide genuine measured benefit.