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:
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:
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/>
:
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?
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:
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:
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:
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:
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()
:
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:
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:
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:
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 hiding 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 clicks it (like this).
That’s all. Thanks for reading.
For more stuff like this follow @pomber on twitter.