Lazy loading (and preloading) components in React 16.6

Lazy loading (and preloading) components in React 16.6
Lazy loading (and preloading) components in React 16.6 React 16.6 adds a new feature that makes code splitting easier: `React.lazy()`.

Let’s see how and why to use this feature with a small demo.

We have an app that shows a list of stocks. When you click on one stock it shows you a chart:

Try it

That’s all it does. You can read the full code in the github repo (also check the pull requests to see the diffs and a running version of the app for each change we’ll do).

For this post, we only care about what’s in the App.js file:

import React from "react";
import StockTable from "./StockTable";

import StockChart from "./StockChart";

class App extends React.Component {
  state = {
    selectedStock: null
  };
  render() {
    const { stocks } = this.props;
    const { selectedStock } = this.state;
    return (
      <React.Fragment>
        <StockTable
          stocks={stocks}
          onSelect={selectedStock => this.setState({ selectedStock })}
        />
        {selectedStock && (
          <StockChart
            stock={selectedStock}
            onClose={() => this.setState({ selectedStock: false })}
          />
        )}
      </React.Fragment>
    );
  }
}

export default App;

We have an App component that receives a list of stocks and shows a <StockTable/>. When a stock is selected from the table, the App component shows a <StockChart/> for that stock.

What’s the problem? Well, we want our app to be blazing fast and show the <StockTable/> as fast as possible, but we are making it wait until the browser downloads (and uncompresses and parses and compiles and runs) the code for <StockChart/>.

Let’s see a trace of how long it takes to display the <StockTable/>:

Trace without lazy loading

It takes 2470 ms to display the StockTable (with a simulated Fast 3G network and a 4x slowdown CPU).

What’s in those (compressed) 125KB we are shipping to the browser?

Webpack Budle Analyzer Report

As expected, we have react, react-dom, and some react dependencies. But we also have moment, lodash and victory, which we only need for <StockChart/>, not for <StockTable/>.

What could we do to avoid <StockChart/> dependencies to slow down the loading of <StockTable/>? We lazy-load the component.

Lazy-loading a component

Using a dynamic import we can split our bundled javascript in two, a main file with just the code we need for displaying <StockTable/> and another file with the code and the dependencies that <StockChart/> needs.

This technique is so useful that React 16.6 added an API for making it easier to use with React components: React.lazy().

In order to use React.lazy() in our App.js we make two changes:

Diff

First we replace the static import with a call to React.lazy() passing it a function that returns the dynamic import. Now the browser won’t download ./StockChart.js (and its dependencies) until we render it for the first time.

But what happens when React wants to render <StockChart/> and it doesn’t have the code yet? That’s why we added <React.Suspense/>. It will render the fallback prop instead of its children until all the code of all its children is loaded.

Now our app will be bundled in two files:

Webpack Budle Analyzer Report

The main js file is 36KB. The other file is 89KB and has the code from ./StockChart and all its dependencies.

Let’s see again how much it takes the browser to show the <StockTable/> with these changes:

Trace with lazy loading

The browser takes 760 ms to download the main js file (instead of 1250 ms) and 61 ms to evaluate the script (instead of 487 ms). <StockTable/> is displayed in 1546 ms (instead of 2470 ms).

Preloading a lazy component

We made our app load faster. But now we have another problem:

Notice the “Loading…” before showing the chart (try it)

The first time the user clicks on an item the “Loading…” fallback is shown. That’s because we need to wait until the browser loads the code for <StockChart/>.

If we want to get rid of the “Loading…” fallback, we will have to load the code before the user clicks the stock.

One simple way of preloading the code is to start the dynamic import before calling React.lazy():


const stockChartPromise = import("./StockChart");
const StockChart = React.lazy(() => stockChartPromise);

The component will start loading when we call the dynamic import, without blocking the rendering of <StockTable/>.

Take a look at how the trace changed from the original eager-loading app:

Eager Loading vs Lazy Loading

Now, the user will only see the “Loading…” fallback if they click a stock in less than 1 second after the table is displayed. Try it.

You could also enhance the lazy function to make it easier to preload components whenever you need:

function lazyWithPreload(factory) {
  const Component = React.lazy(factory);
  Component.preload = factory;
  return Component;
}

const StockChart = lazyWithPreload(() => import("./StockChart"));

// somewhere in your component 
...
  handleYouMayNeedToRenderStockChartSoonEvent() {
    StockChart.preload();
  }
...

Prerendering a component

For our small demo app that’s all we need. For bigger apps the lazy component may have other lazy code or data to load before it can be rendered. So the user would still have to wait for those.

Another approach for preloading the component is to actually render it before we need it. We want to render it but we don’t want to show it, so we render it hidden:

class App extends React.Component {
  state = {
    selectedStock: null
  };
  render() {
    const { stocks } = this.props;
    const { selectedStock } = this.state;
    return (
      <React.Suspense fallback={<div>Loading...</div>}>
        <StockTable
          stocks={stocks}
          onSelect={selectedStock => this.setState({ selectedStock })}
        />
        {selectedStock && (
          <StockChart
            stock={selectedStock}
            onClose={() => this.setState({ selectedStock: false })}
          />
        )}
        {/* Preload <StockChart/> */}
        <React.Suspense fallback={null}>
          <div hidden={true}>
            <StockChart stock={stocks[0]} />
          </div>
        </React.Suspense>
      </React.Suspense>
    );
  }
}

React will start loading <StockChart/> the first time the app is rendered, but this time it will actually try to render <StockChart/> so if any other dependency (code or data) needs to be loaded it will be loaded.

We wrapped the lazy component inside a hidden div so it doesn’t show anything after it is loaded. And we wrapped that div inside another <React.Suspense/> with a null fallback so it doesn’t show anything while it’s being loaded.

Note: _hidden_ is the HTML attribute for indicating that the element is not yet relevant. The browser won’t render elements with this attribute. React doesn’t do anything special with that attribute(but it may start giving hidden elements a lower priority in future releases).

What’s missing?

This last approach is useful in many cases but it has some problems.

First, the hidden attribute for hidding the rendered lazy component isn’t bulletproof. For example, the lazy component could use a portal which won’t be hidden (there is a hack that doesn’t require an extra div and also work with portals, but it’s a hack, it will break).

Second, even if the component is hidden we are still adding unused nodes to the DOM, and that could become a performance problem.

A better aproach would be to tell react to render the lazy component but without comitting it to the DOM after it’s loaded. But, as far as I know, it isn’t possible with the current version of React.

Another improvement we could do is to reuse the elements we are rendering when preloading the chart component, so when we want to actually display the chart React doesn’t need to create them again. If we know what stock the user will click we could even render it with the correct data before the user click it (like this).

That’s all. Thanks for reading.

Learn ReactJS: Code Like A Facebook Developer

Projects In ReactJS - The Complete React Learning Course

Learning ReactJs Data Visualization

React Native With An Express/MongoDB Backend

React and Flux Web Development for Beginners

Suggest:

Getting Closure on React Hooks

Build a full stack application with Node-RED

JavaScript for React Developers | Mosh

React + TypeScript : Why and How

Web Development Trends 2020

Build a React.js application and add Authentication with Auth0