The Energetic React Lazy

May 28, 23

You might have heard about React lazy and suspense before, but might never have felt the energies ⚡️ it has, I don’t know why is it called lazy why not energy or maybe flash, I guess it’s called as lazy since it lazily loads something 🤔.

The Energetic React Lazy - cover

Introduction

So a react app will have all the code bundled using webpack, rollup, esbuild or something other bundler. The basic idea behind how any bundler works is that - it picks up all the source code files for your app and bundles them (merge them) into a single file (here the main.js file) and then you typically serve your app with this file - containing react source code, app.js, material.js, antd.js and all underlying dependencies as well. These bundles are fine upto a level - a level when your app starts growing - having tons of dependencies and your own source code, and hence this bundle keeps on growing in size. The larger these bundles are, the longer it takes to load these assets on the user’s browser and hence deteriorating your app performance.

Single Bundle

And this is how the network waterfall looks like for this react app - Only 1 file, but that is of about 90kB, and trust me this just keeps on growing, I have seen this single bundle file to grow as much as 800kB (yes with gzip).

Single Bundle Waterfall

Code Splitting

So as to address this behemoth 🐘 problem and provide a better solution, teams and bright minds came up with the idea of Code Splitting - which simply dictates that only ship the code that is necessary for the app/page. As simply as that, so the example you see above where all of the various modules and dependencies are bundled into 1 or maybe 2 final bundles, they are split into multiple smaller chunks and only fetching the ones required by the page/view. That simple and easy, right?

BTW, throughout this blog you can refer to this example repository - react-lazy-example - which is a very basic create-react-app with Material UI and Ant Design added to elaborate the concept of bundle splitting.

Also, since the launch and use of HTTP/2 it has been much easier and faster to make multiple network calls from the web app, so code splitting (even aggressively) made more sense - split the app into as smaller but as meaningful pieces as possible and keep the network load smaller and smaller.

Let’s look at our example network waterfall with some code splitting implemented.

Multiple Bundle Waterfall

Wait…what! What are these 5 new files popping up?

Actually it is the same app, but with code-splitting. So what is happening is, the first two bundles - main and 369 are for the home page, one of them containing the dependencies (obviously main.js) and the other one containing the app source code.

Similarly the next two 722 and 107 are the bundles fetched when you open the next page - which is /material from the example app, 722 contains the dependencies and 107 the source code, and the last two are for the other page /antd again only fetched when you visit the page.

If you sum up the total size of bundles you fetched it will be around 95-97kB which will sometimes be higher than the single bundle - because of some scaffolding code here and there, but get the energising idea previously it required 91kB for the home page, but now it only required 56kB, almost half the size. And none of the code for material ui, ant design or any other dependencies not a part of the home page were served, magic no 🪄.

What just happened is this - all of these source code (entry points) are bundled separately.

Multiple Bundles

React.lazy

Let’s talk about how to do this code splitting with React - without worry about what bundler is there under the hood, configuring webpack or rollup.

React.lazy is an intuitive function that let’s you dynamically import any module or component and render it as a regular JSX Component.

The API looks very simple and uses something called as dynamic imports to enable - code splitting, lazily fetching, dynamic importing.

// Before
import HomePage from './HomePage';

// After
import {lazy} from 'react';

const HomePage = lazy(() => import('./HomePage'))

…and that’s it, nothing else needs to be changed on how and where you are rendering these components (actually one place, we will talk about it) and you are all setup and done.

Checkout this pull request - https://github.com/maddhruv/react-lazy-example/pull/2/files to view the full changes for the example app.

Yes that was all required to enable code splitting in react, isn’t that an awesome and energetic feature from react? I have seen many teams and app not implemented this, even though this is like a 1 pointer ticket on your Jira.

So what is that other piece called Suspense? Oh yeah →

Suspense

As basic as it may sound - Suspense is a react component that renders the component passed into the fallback prop unless a certain condition is met - and with React.lazy that condition is that until the dynamic import is completely loaded, remember I said loaded and not rendered. By the time react is fetching the bundles for the new component with lazy, it shows the fallback which usually is being set to some Spinner or Shimmer or maybe a Splash Screen.

Small React.lazy fix

Well, recently working on a React application and implementing React lazy into it, I faced a very weird problem. This app is not hosted on any CDN and the deployment is as simple as - build - copy the artefacts - replace the old ones with the new one, as simple as that. Well actually this was fine until we had a single bundle containing all the code for app, so whenever someone goes to a new page, no new bundle was fetched. And if you might have observed from the bundle names from the screenshots above - the names contain some random string in them like main.9678aa.js, actually these are not some random strings but contenthash evaluated based on the content of the bundle and changes whenever the content changes. But why is that even there? to enable assets caching in browser and ensure any changed bundles have a different name and the browsers don’t simply use the old cached response for it. If your document requires a file main.js the browsers will automatically serve the cached response everytime no matter if the actually source of the file changed on the server, but if the document refers to the new bundle as main.1.js and it won’t be available with the browser cache it will fetch from the server.

Coming back to the original issue, assume any user had the app loaded sometime in the early morning and came back to use it in the afternoon, but if in the middle of that the app gets deployed - publishing the new builds, so what happens is → the first time the app was loaded, any other page links with React.lazy had a reference to some bundle → let’s say about.123.js so whenever the user would click on the about link, the app will try to fetch this file and use it, but in this very special case if the file on the server actually got updated, to let’s say about.456.js hence the actual network call with fail with a 404, isn’t it? and the app will crash 🐛. To address this issue, we have written a very minimal wrapper around React.lazy that simply ensures if the bundle fails to load, it reloads the whole app, so that all the references to the bundles are fresh and are available on the server.

import {lazy} from 'react';

export const lazyWithRetry = (componentImport) =>
lazy(async () => {
try {
return await componentImport();
} catch (error) {
console.error(error);
return window.location.reload();
}
});

// Usage
const HomePage = lazyWithRetry(() => import('./HomePage'));

What it does is basically tries to load the asset within a try...catch block, and if throws any error, it just simply reloads the whole app. I know CDNs can solve this problem with a better way, but just in case your app doesn’t use a CDN or the CDN is down itself. If there is a better solution or enhancement to this, please comment below and let me know, happy to discuss.

Reference

Refer to these documentations and blog on more details and examples around code splitting and react lazy.