Optimize performance with React.lazy and Suspense
Tags: react, performance, webdev
Published: 2023-04-05 10:00:00

## Why would you need code-splitting
Typical React app has its files "bundled" into a single file. Tool like [Webpack](https://webpack.js.org/) follows imported files and merges them into a "bundle". The bigger the bundled file, the longer it takes for your app to load. Frontend developers should be extremely aware of this fact - especially when including large third-party libraries.

When user starts to use your app he probably doesn't need all pages and components at once. Moreover, there is some code that the user may never need. Now, imagine that we can control which part of code are loaded so we can dramatically improve the performance of our apps.

To better understand which parts of the bundle is used by the user you can use the ["Coverage" tool](https://developer.chrome.com/docs/devtools/coverage/) from the Chrome Dev Tools.

## Real world use-case
The most common use-case for this in React is lazily loading route elements. Using this technique, pages that are not required on the home page can be split out into separate bundles, thereby decreasing load time on the initial page and improving performance.

Imagine that you have an app with multiple routes (pages) and they are loaded at once with one large bundle. The user experience isn't that great - user has to wait for all pages to be loaded even if he only wants to see the Dashboard or just one another route. With lazy loading we can at first load the Dashboard page and then load another pages on-demand. That dramatically decreases the time of loading the first screen of our application. 

## Application setup
For our example application we will have Home screen on `/` path and About screen on `/about` path. I will be using `create-react-app` with `react-router-dom@6`. I've created two simple components/pages named `Home` and `About` exported as default exports.

> **Important!** React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component.

I also added `Layout` component with simple navigation: 
```jsx
function Layout() {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>
      <hr />
      <Outlet />
    </div>
  );
}
``` 

And `App` component looks has the following content: 
```jsx
import Home from "./pages/Home";
import About from "./pages/About";
import Layout from "./components/Layout";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route
          path="about"
          element={<About />}
        />
      </Route>
    </Routes>
  );
}
```
With that setup we have single, large bundle and all pages are loaded during the initial load. For this particular case it won't be a problem - `About` component has only two elements with plain text and it doesn't use any sizeable libraries. But we assume that our app is taking too long to load and we want to fix that as soon as possible. 

According to our plan we want to have `Home` at the initial load and then load `About` on-demand.

## React.lazy

Let's start with changing the way how we import the `About` component:
```jsx
// import About from "./pages/About"; ❌
const About = React.lazy(() => import("./pages/About")); ✅
```
We've used React's build-in support for loading modules as React components. When we run the app we would see the error. This is caused by the fact that while the component is lazily loaded React can't display anything in its place. 

## Adding Suspense

In React we can fix this issue using `<Suspense />` component. Using `<Suspense />` allows us to render a fallback value while the user waits for the module to be loaded. Just wrap `About` with suspense and provide the fallback value. In place of `Loading...` screen you can pass any valid React component such as loading spinner.
```jsx
<Route
  path="about"
  element={
    <React.Suspense fallback={<>Loading...</>}>
      <About />
    </React.Suspense>
  }
/>
```
With the following fix user should see `Loading...` in the place where the `About` component should be displayed while loading the module. 

## Comparison
Now let's check how our changes affected the overall user experience. For demonstration purposed I've installed `lodash` imported it in the `About` component. I've set network throttling to *Slow 3G*.

At first let's take a look at loading time and bundle size for the non-optimized app (no `React.lazy` and `Suspense`):
![React.lazy (unoptimised).png](https://media.graphassets.com/IZwE39TbTECiQngNzRGE)

As you can clearly see we have a single `bundle.js` file which includes all files for the app. User had to wait almost 12 seconds to use the app, at least he didn't had to wait to see the `About` page. However if he wanted to see just the home page he lost few seconds waiting for the whole bundle. There were over 470 kB transferred at once.

Now let's perform similar test but using `React.lazy`:
![React.lazy (optimised).png](https://media.graphassets.com/qgEXxYOBRbufHdY6l1SG)
Here you can see the results after navigating to the `/about` path. Initially there were 375 kB to transfer which took 2 seconds less than in the previous case. Used had to wait less to interact with the app. Unused `lodash` package wasn't transferred at all. 

After clicking the link to `/about` another scripts were loaded: `lodash` and chuncked file with `About` component. The size of transferred JS files is the same in both cases but the initial load time is shorter for the lazily loaded content. 

## Summary
In larger, production applications those differences between using lazy loading and not can be much greater. Here we had only simple components with almost no content. However the difference is noticeable even in this situation.


You can optimize the imports even further to improve the user experience - for example you can conditionally lazy load components on mouse hover or on app's state change.