Splitting React bundles at the component level
Shipping a single large JavaScript bundle to users on their first page load is a common performance pitfall in React applications. The more components, utilities, and third-party libraries an app includes, the bigger that initial payload becomes, which directly impacts how long it takes for the page to become interactive—especially on slower devices and network connections.
The React.lazy method addresses this by letting you code-split a React application at the component level using dynamic imports. Instead of loading everything up front, components are fetched only when they are actually needed.
Pairing lazy loading with Suspense
Once you split a component out of the main bundle, there is always a brief delay while that code is fetched over the network. To keep the UI responsive during that gap, you can combine React.lazy with the Suspense component, which accepts a fallback prop that renders any React component as a loading state.
For example, an avatar component can be rendered only when a user clicks a button. At that moment, the app makes a request for the code needed by the suspended AvatarComponent, and the fallback loading indicator is shown in the meantime.
If you want to see this behavior in action, open the app in DevTools, switch the Network tab's throttling setting to Fast 3G, and then trigger the component load. The loading indicator will remain visible for longer, and you will see the code for AvatarComponent arrive as a separate chunk.
Small components load quickly, so the spinner may be barely visible. For larger components on a weak connection, the loading state becomes far more noticeable, which is exactly why a well-designed fallback matters.
Suspending several components at once
Suspense can also wrap multiple lazy-loaded components simultaneously. When all of them are suspended inside a single Suspense boundary, React delays rendering the entire group until every component within it has finished fetching. Users see one loading state instead of a series of staggered indicators as each piece of the UI pops in independently.
This approach avoids the jarring experience of different parts of a page appearing at different times, each with its own spinner.
Recovering from loading failures
Suspense handles the temporary loading state, but it does not cover the case where a network request fails entirely. You may be offline, or the app may be trying to lazy-load a versioned URL that is no longer available after a server redeployment.
To handle those failures gracefully, use an error boundary. Any React component can act as one if it implements either static getDerivedStateFromError() or componentDidCatch(). Wrap your Suspense component in a parent error boundary, then render its children as-is when there is no error, or show a custom error message when something goes wrong.
Where to start with code splitting
- Begin at the route level. Routes provide the simplest way to identify natural split points, and the React documentation demonstrates how
Suspenseworks withreact-routerfor route-based splitting. - Look for large components that render only in response to specific user interactions, like clicking a button. Splitting these can have an outsized effect on your initial JavaScript payload.
- Consider deferring anything offscreen or not essential for the user's first interaction.



