React 16.6 adds a new feature that makes code splitting easier:
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:
For this post, we only care about what’s in the
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
Let’s see a trace of how long it takes to display the
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
What could we do to avoid
<StockChart/> dependencies to slow down the loading of
<StockTable/>? We lazy-load the component.
Lazy-loading a component
<StockTable/> and another file with the code and the dependencies that
This technique is so useful that React 16.6 added an API for making it easier to use with React components:
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
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
The component will start loading when we call the dynamic import, without blocking the rendering of
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
lazyfunction 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
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.
hiddenis 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).
This last approach is useful in many cases but it has some problems.
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.