Blog

Bundle splitting with React’s lazy & Suspense

A guide to using React suspense and lazy loading components

When creating an application using a framework like React, bundle sizes are important. They can grow fast, especially when using a lot of external packages. Most of the the time the application doesn’t need all these packages up front, but only when they’re used in the application. To achieve this we can use bundle splitting provided by React using React.lazy().

Analyse your bundle & identify large components

We know that the bundle size is large, but how do we know which components to blame? webpack-bundle-analyzer is the perfect tool to investigate that. When using create-react-app, you can add this scripts to your package.json:

// generate build including bundle-stats.json
npm run build -- --stats

// analyse bundle-stats.json
webpack-bundle-analyzer ./build/bundle-stats.json 

The analyser produces an interactive web page that looks like this:

Analysis of the bundle without any bundle splitting
Analysis of the bundle without any bundle splitting

We see that some packages take up a lot of space. They will get loaded and parsed before the application mounts in the DOM, but it’s possible they’re not needed on the page we visit. It would be nice to separate them into their own bundle, and load them only when needed.

Code splitting in React can be achieved by something called ‘dynamic imports’ and React.lazy, which looks something like this:

import * as React from 'react'
import Users from './Users'

const Posts = React.lazy(() => import('./Posts'))

// rest of component file

This automatically results in a separate bundle for the Posts component, which then gets loaded asynchronously when it is used in the application.

Below you see the result of bundle splitting in the bundle shown earlier in this post (screenshots from the componentizer app):

Route-level bundle splitting
Route-level bundle splitting
Route & component level bundle splitting
Route & component level bundle splitting

How does this work?

To be clear about what happens when using React.lazy we’re going to look at webpack’s code splitting, one of its more powerful features. It is used to split up code in various bundles to reduce bundle sizes and load times. The most convenient way to achieve it is by using dynamic imports. These are inline function calls from within modules/components. An inline function call looks like this:

const User = import('./User')
// Results in 1.chunk.js

const User = import(/* webpackChunkName: 'User' */ './User')
// Results in User.chunk.js

In React, just importing components like that will not work. This is where React.lazy comes in. This is a function that let's you use your dynamic imports as a regular components.

const User = React.lazy(() => import('./User'))

Preloading

Sometimes we know (or can try to predict) if a component is going to be used. In that case we want to use prefetch or preload, depending on the situation. You can indicate this to Webpack by using ‘magic comments’.

import(/* webpackPrefetch: true */ './myComponent')

// The above line results in a link tag appended to the <head>
<link rel="prefetch" as="script" href="/static/js/2.chunk.js">

There are two options here:

  • webPackPreload: Indicating that the browser will need this resource soon after loading, using link preloading. This can be used at for example fonts, important images or scripts that are needed on the current page.
  • webPackPrefetch: uses browser idle time to load resources it might need in the future, using link prefetching. This is used for fetching resources and put them in cache, so when they are used on the next page for example, they load instantly.

Suspense

Components imported using React.lazy must be wrapped inside a Suspense tag. This is a feature of React that defers rendering until the lazy loaded component is fetched. Suspense needs to be placed anywhere above the lazy loaded component in the component tree. It provides a fallback for when a component inside it hasn’t loaded yet, using the required ‘fallback’ prop. The beautiful thing about Suspense is that it doesn’t have to be inside the component where something is imported, but can also be higher up in the component tree. In the example you see how Suspense works when this is the case:

If we run this example and take a look in our devtools, we see that the seperate components appear as their own javascript file. The loading state only gets replaced if all the components in the Suspense tag are loaded:

Chrome devtools showing how files are lazy loaded

Things to look forward to

In the current stable release of React (17), Suspense is limited to use with React.lazy. This a nice feature for now, but what’s coming is even more exiting: loading data with Suspense! This means we can use Suspense with fetching JSON or images. Having more control over loading states when data fetched will result in better loading states in UI’s.

To learn more about Suspense and what’s coming to react in the future, watch this amazing talk by Dan Abramov: https://www.youtube.com/watch?v=nLF0n9SACd4.

Further reading:

    ← All blog posts