Kushagra Gour's Website
Home โ†  Blog
Lab โ€ Games โ€ Blog โ€ Me

How I saved 100KB with React.lazy

This post is about how I was able to reduce my homepage JavaScript by more around 100Kb.

TL;DR: Use React.lazy and React.Suspense to lazy load your non-critical dependencies

I have a React app bootstrapped from create-react-app. One of the page (just a React Component) in that app uses CodeMirror (a code editor). The app uses react-router for routing. And so this page component, just like all other pages, is imported in the main App component to provide to the router.

App.js

import Home from "./Home";
import Page2 from "./Page2";

function App() {
<Router>
<Route path="/" component={Home} />
<Route path="/page2" component={Page2} />
</Router>;
}

Page2.js

import CodeMirror from "react-codemirror";

function App() {
return (
<div>
<CodeMirror />
</div>
);
}

Note: react-codemirror actually does a named export. But for simplicity, I am assuming a default export.

This situation leads to Page2.js being a synchronous dependency to render App.js. And Page2.js in turn depends on react-codemirror. So indirectly, react-codemirror becomes a synchronous dependency to render App.js. This basically means whatever page we visit, react-codemirror will be fetched and parsed before the page renders. Even if Codemirror is not even being used on that page! Let's fix this.

Solution

The solution is pretty neat and easy. React recently introduced a new API: React.lazy. And an accompanying component called Suspense. Here is how we use them to fix our problem.

Step 1: Make the import lazy

Page2.js imports react-codemirror. Ideally we want that Page2.js should load react-codemirror asynchronously when Page2 is actually visited.

This is our current Page2.js:

import CodeMirror from "react-codemirror";

function App() {
return (
<div>
<CodeMirror />
</div>
);
}

Using the React.lazy API, we can make the import lazy. Like so:

import React from "react";
const CodeMirror = React.lazy(() => import("react-codemirror"));

function App() {
return (
<div>
<CodeMirror />
</div>
);
}

And this just starts working out of the box! No more change required in the way CodeMirror component is used. What you'll notice now is initially when you are on homepage, CodeMirror doesn't load. When you visit /page2/, you see blank area where CodeMirror was to be rendered, for a short duration while CodeMirror loads asynchronously. And then when it has finished loading, CodeMirror component renders.

While CodeMirror is being fetched, there is just blank space where CodeMirror editor should have been present. That is not such a good experience as user is left without any information about that blank space. That is where React.Suspense component comes into action.

Step 2: Improve the blank space context

Here is all we need to do to make the experience better:

import React, { Suspense } from "react";
const CodeMirror = React.lazy(() => import("react-codemirror"));

function App() {
return (
<div>
<Suspense fallback="Loading editor...">
<CodeMirror />
</Suspense>
</div>
);
}

We wrap the asynchronous/lazy components with a Suspense tag and give it a fallback which should show instead of blank space. That is it!

Bonus tip

There is one special requirement for using React.lazy which you need to be aware of. It only works with component that have default export. So components with named exports can't be lazily imported with it. But you might have components with named exports, what to do then? There is a small trick. Let's assume our Page2.js file exported Page2 component so that it was initially imported as import {CodeMirror} from 'react-codemirror'. In that case, we can use React.lazy on it as follows:

import React, { Suspense } from "react";
const CodeMirror = lazy(() =>
import("react-codemirror").then(module => ({ default: module.CodeMirror }))
);

function App() {
return (
<div>
<Suspense fallback="Loading editor...">
<CodeMirror />
</Suspense>
</div>
);
}

What we did here is once we import the named module, inside the then callback we turn it into a seemingly default exported module - an object with the module available on default key.

That's all folks! Go shave off some unnecessary bytes of your pages. If you have any questions or comments, ask me on Twitter @chinchang457 (DMs are open).