So, you've finished your project, an outstanding yet simple web application or website that also looks awesome, performs great and you're really happy with it.
The moment has come for you to share it with a peer, so you have set up a meeting at your favorite neighborhood coffee shop. You both arrive at the meeting, connect your laptops to WiFi and start browsing your project in order to discuss it.
Oh no!...something is wrong :/ It seems that your awesome web application is taking forever to load. You both stare at the white page, waiting patiently for the UI to load, hopefully, it will finish loading in no time. But why? What happened? You're thinking, this application is just a bunch of react components on a page, no backend yet, nothing really complicated to load or go wrong...it was working just fine on my machine you said ¯\_(ツ)_/¯
Your solid internet connection is taken for granted
What would happen if our web application is deployed to the cloud or some other hosting service? It is now live on production and available for everyone to use. globally.
Let's say some potential user is browsing your application. The "problem" is that this user lives in Venezuela, which happens to be ranked somewhere at the bottom of this list, in terms of internet connection speeds. Or maybe this user is in the same region as you but is using their home WiFi with a laggy internet connection. This user is finally getting the first page of your application, but it took them forever to get it, and to make things even worse, you didn't have the chance to implement a loading animation or similar UX solution. We know this is a bad UX, right?
Conclusion: Our users not necessarily have the best or even average internet connection and we should take that into consideration when thinking about UX and developing the front end of our application.
The root cause: bundle size
Our front end is bundled with webpack or a similar tool for a production build. In case our application has grown and we take a closer look at our bundle we can see it is probably quite heavy, which could be a problem with a poor internet connection. The browser is fetching that bundle from the server which might take some time, depending on connection speed, server configuration, load, and more. In the meantime, our users will just have to wait.
Bundle splitting
What if we had an option to improve the UX and make our application initially load a lot faster? Well, we do have a good option. Webpack allows us to introduce bundle splitting. We can split our bundle into one or more chunks in strategic points. What this means in practice, is that we will have to tell webpack where to split our bundle. But what does it mean? if we split the bundle won't our application break? How will react handle this? Won't we have some missing parts between 2 or more bundles of our application? No. Webpack along with React allows us to introduce lazy loading, which is basically loading some parts of our application only when needed, or when the user will have to use them. This effectively reduces the initial bundle size. Let's assume it contains only the first page of our application. Only if the user will navigate to a different page or section of our UI, react will load the corresponding component, which under the hood tells the browser to fetch related bundles created by webpack earlier in the build process.
If you're not using create-react-app, you may need to set up webpack for bundle splitting to work as you would expect. Your weback configuration should look similar to the following example:
module.exports = {
entry: {
main: './src/app.js',
},
output: {
// `filename` provides a template for naming your bundles (remember to use `[name]`)
filename: '[name].bundle.js',
// `chunkFilename` provides a template for naming code-split bundles (optional)
chunkFilename: '[name].chunk.js',
// `path` is the folder where Webpack will place your bundles
path: './dist',
// `publicPath` is where Webpack will load your bundles from (optional)
publicPath: 'dist/'
}
};
Lazy loading
Lazy loading react components or modules is as simple as importing them with a special lazy function, part of react:
import React, { useState, Fragment } from "react";
//import MyComponent from './my-component';
const MyComponent = React.lazy(() => import("./my-component"));
const App = () => {
const [isVisible, setIsVisible] = useState(false);
return (
<Fragment>
<span>Component is {isVisible ? "visible" : "not visible"} </span>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle my component
</button>
{isVisible && <MyComponent />}
</Fragment>
);
};
Keep in mind that MyComponent
must be exported as default for React.lazy
to work properly.
But wait, what happens after using this approach? if you take a look, you will notice that the component is imported dynamically and not statically, which means it is not available to render right away.
This also means that once the page is loaded, that piece of UI my-component is responsible for is obviously not rendered yet. In addition and most importantly, after clicking the Toggle my component button, it may take some time for your lazy component to load, depending on its implementation and how heavy it is. The user doesn't get any feedback on how long the wait is going to be, or when it will be over and the missing UI will finally render.
Let's take an extreme case where your component is actually a really complicated piece of UI with lots of dependencies. In that case, the loading time might be significant due to the split chunk weight, meanwhile, the user just waits without any clue on how long.
FYI if you're using a modern version of create-react-app
, using the example above will result in a Suspense-related error because you may forget to use theSuspense
component. In our example, Suspense was left out on purpose, in order to illustrate how easy it is to lazy load components and that using Lazy loading without Suspense is technically possible. create-react-app is very opinionated on UX best practices and we are going to learn more about it and why this error is in place, in the next section.
Real world example of multiple chunks loading
Naming your chunks
Webpack supports a special comment directive that will be used to name our split chunks
const MyComponent = React.lazy(() => import(
/* webpackChunkName: "MyComponent" */
"./my-component"));
Lazy loading with react Suspense
The react core team has come up with an elegant solution to the situation where the user waits for something to load: A special Suspense
component. This component is available as of react v16.6 and accepts the lazy component(s) as children, and a fallback prop for the UI you would like to render while loading is in progress.
This time, the user will know something is loading. Once loading is finished, Suspense seamlessly replaces the fallback component with the actual component that was loaded.
Suspense allows us to lazy load components in a declarative coding style.
import React, { Suspense, useState } from "react";
const App = () => {
const [isVisible, setIsVisible] = useState(false);
return (
<Suspense fallback={<span>Loading...</span>}>
<span>Component is {isVisible ? "visible" : "not visible"} </span>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle my component
</button>
{isVisible && <MyComponent />}
</Suspense>
);
};
If for some reason, the loading process is canceled by the user, the browser is still fetching the bundle so next time the component will render immediately. In addition, once the bundle has been fetched and cached by the browser, Suspense will not use the fallback, and render will occur immediately.
What to render as a fallback
The current UX trend is to use some kind of a placeholder animation while loading pieces of UI. react-content-loader is a popular npm package for this use case. It is customizable, supports react and react-native
, has some bundled presets, and actually supports SVG animation out of the box.
Illustration of a loading animation
Most modern design tools support exporting design directly as SVG which can be used with react-content-loader
. If you want to get your hands dirty and do it yourself, Method Draw is an excellent web tool that you can use to design your fallback placeholder animation. It supports exporting as SVG and it's even open source!
Method Draw - a simple and easy vector editor for the web
Using a fallback only when we have to
In most cases our network speed is pretty solid, so we might encounter a situation where the fallback renders for a split second - that is the amount of time it took for the browser to download the bundle, even though the user might not need it in order to get a good UX from our app. This fast switching between the fallback and the actual component may seem like a bug, which is not good.
Fortunately, we can add some logic to render our fallback only when we feel it is a must, meaning after a minimum period of time. Here is a more real-world example:
//DelayedFallback.js
import React, { useEffect, useState } from 'react';
import ContentLoader from 'react-content-loader';
export const DelayedFallback = ({ children, delay = 300 }) => {
const [show, setShow] = useState(false);
useEffect(() => {
let timeout = setTimeout(() => setShow(true), delay);
return () => {
clearTimeout(timeout);
};
}, []);
return <>{show && children}</>;
};
//Header.js
import { DelayedFallback } from './DelayedSuspenseFallback';
import { SuspendedCreateMenu } from './CreateMenu/CreateMenu.suspended';
expor const Header = (props) => (
<Suspense
fallback={
<DelayedFallback>
<SuspendedCreateMenu
...
/>
</DelayedFallback>
}>
<CreateMenu
...
/>
</Suspense>
);
//CreateMenu.suspended.js
export const SuspendedCreateMenu = (props) => {
return (
<ContentLoader
viewBox="0 0 1155 381"
backgroundColor="#f4f4f4"
foregroundColor="#d4d3d3"
speed={2.1}
>
<rect
stroke="null"
rx="9"
id="svg_3"
height="59.87731"
width="371.44229"
y="78.98809"
x="289.67856"
strokeOpacity="null"
strokeWidth="1.5"
fill="#ececec"
/>
// Rest of your SVG goes here
...
</ContentLoader>);
}
As you can see, the fallback is just an SVG wrapped with a ContentLoader
component imported from the react-content-loader package.
It is also worth mentioning that we are naming our fallback component the same as our actual component but with a .suspended
suffix. This is not mandatory.
A major UX change
Lazy loading components is a big UX change for our users. Instead of waiting for the initial load and then interact freely with the application, introducing lazy loading actually means that initial loading time will be minimal but interacting with the application may include subsequent loading. A good architecture and UX design will result in a seamless and pleasant experience but keep in mind that it may require some joint planning of UX and development.
Don't rush into bundle splitting
Before splitting our bundle we should exhaust all our efforts and try to reduce our bundle size. Sometimes we might find it contains a lot of unnecessary user code or dependencies.
A popular dev dependency we can use just for this is webpack-bundle-analyzer
. This is a webpack plugin that can generate a bundle report, which allows us to dive into the content of our bundles in a visual manner, analyze them, and get some good insights.
A bundle report (credit: webpack-bundle-analyzer)
Debugging Suspense with react devtools
When you start working with bundle splitting, Suspense, and lazy loading, you will soon find out that you need a quick and reliable way of simulating poor network speed, going back and forward between suspension modes, and examining what is actually loaded by the browser behind the scenes.
In order to do that, you should get familiar with the network panel (assuming you're using Chrome) and the react Components dev tools panel.
Network panel
This panel of Chrome dev tools lets your overview all of your web requests. We're currently specifically interested in the requests the browser is making to fetch our bundles which are javascript code (or chunks). For this purpose, you might want to filter the visible requests using the JS preset at the top bar. You can also filter out what is not important by typing anything in the filter input. (tcp
in the example).
In order to simulate a slower connection, you can choose one of the presets from the Throttling dropdown. The default is Online. You can also add additional presets of your own which is great.
Toggling between Online and Slow 3G etc, helps us test our application behavior at different network speeds. In practice, this may or may not trigger our Suspense fallback.
An example of JS assets loading summary
It is highly recommended to get familiar with the Netowrk panel which offers a lot more options and valuable insights, unfortunately, they are out of scope for this post.
React developer tool
The React developer tool is a Chrome extension that augments our dev tools with a Components panel. This panel deserves a post on its own, but for the scope of this post, let's focus on the options to search for a component in our application components tree and toggling Suspense instances.
You can search for your suspended component using the top search input. Once you find a component that contains a suspended content, you will notice a Suspended toggle you can use to switch your fallback on and off without reloading your page or making any new requests to the server.
An example a components tree with Suspended toggled on
Final words
We've learned how to dynamically load components using Suspense, what actually happens behind the scenes when we split our app bundle into chunks, render a fallback component while the user waits for loading to finish.
We've also briefly discussed how big UX change lazy loading is causing in our application and that sometimes we may want to examine our bundle before making a step forward.
Lastly, we saw an example of what to render as a Suspense fallback, how to create this fallback component, and finally, how to debug things in an efficient way using community tools.
Remember, being lazy is not necessarily a bad thing :)
Top comments (1)
Very nice and complete article. Taking care of the whole process of bundle splitting from beginning to end.