TABLE OF CONTENTS

Improve Overall User Experience and Performance With React Code Splitting

Author Sagi Liba
Sagi Liba on Aug 21, 2020
12 min 🕐

Every Front-end developer should know how to use code splitting. Overtime every Single Page Application size grows and the size of the bundle will grow accordingly. To tackle large bundle sizes and still be able to provide a performant experience without increasing load times, you must know how to split your bundle into separate parts.

This article will go over the following subjects:

  • What is a bundle.
  • The performance costs.
  • Rules of laziness.
  • Code splitting.
  • Three ways to use code splitting:
    • Code split per component.
    • Route based code splitting.
    • Code split related components by grouping.
  • Preloading imports.
  • Naming our split bundles with magic comments.
  • Code coverage.
  • Importing routes dynamically.

What Is A Bundle

When working on a react application, webpack is configured out of the box. When you decide to build your project for production, webpack will bundle your entire application into a single file, AKA a “bundle”, that will hold your entire project.

Code splitting is a feature that is supported by webpack and can create multiple bundles that are loaded dynamically at runtime.

The Two Major Costs Of Performance

First is the cost of sending the code to the user’s browser. Second is the cost of parsing and executing the Javascript we have received.

In both cases the less code we send/parse the better, if we send a smaller bundle the user will receive the code faster, and we will spend fewer resources on parsing and executing it. Your goal should be an initial bundle that is uncompressed and its size is less than 200KB.

Imagine a web application that is 3MB in size, the client will have to wait for the download to complete, and then wait for the parsing to finish. Many users won’t wait that long, especially mobile users, and will already leave your site.

Click here to learn how to compress your server’s responses, and immediately reduce up to 70% of your responses size.

Rules Of Laziness

When it comes to performance being lazy is the ultimate goal, there are two rules when it comes to laziness:

  1. If we don’t do something, we won’t spend time and resources on it.

Example: If a user on an e-commerce site will never enter during his visit into their profile page, contact form, etc.. then why load it in the first place?

  1. If we can do something later, then we don’t have to do it now.

Example: If the user needs to open a modal / work with a rich text editor, why load it when the application starts and not only when it is needed?

Code Splitting

After this introduction to bundling, the cost of performance and laziness its time we start to learn how to use code splitting. Let’s start with a basic example, the way we usually import modules into our code is by using the following syntax:

REGULAR IMPORT

Copy
import Utils from './UtilityFunctions';
console.log(Utils.uniq([2, 1, 2]);); // => [2, 1]

Code splitting allows us to use the dynamic import, which uses the following syntax:

Copy
import("./UtilityFunctions").then((Utils) => {
console.log(Utils.uniq([2, 1, 2])); // => [2,1]
});

When webpack comes across a dynamic import it automatically knows that the modules and the following code should be moved into a separate bundle.

The dynamic import must return a Promise which will be resolved into a module with a default export containing a react component.

Three Ways To Use Code Splitting

React introduced Lazy and Suspense which makes it much easier to use code splitting.

Lazy will be used to automatically load the split bundle with the containing default export.

Suspense will be used to provide a fallback component which will tell the user it is now currently loading the component.

Code Split Per Component

Copy
const LoginPage = React.lazy(() => import("./LoginPage"));

React.lazy takes a function that must call a dynamic import. As mentioned earlier the dynamic import must return a Promise containing a default export of the chosen component.

The lazy loaded component should then be put inside a Suspense component which will provide the loading indicator until the lazy component loads.

  • without using Suspense you will get an exception thrown.
Copy
import React, { Suspense } from "react";
const LoginPage = React.lazy(() => import("./LoginPage"));
function App() {
return (
<div>
// fallback accepts any valid react element
<Suspense fallback={<div>Loading...</div>}>
<LoginPage />
</Suspense>
</div>
);
}

The above example lazy loads a component named LoginPage and provides a loading indication until it finishes loading.

It is possible to render a fallback / loading indicator, for multiple lazy loaded components with a single Suspense component:

Copy
import React, { Suspense } from "react";
const LoginPage = React.lazy(() => import("./LoginPage"));
const RegistrationPage = React.lazy(() => import("./RegistrationPage"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<section>
<LoginPage />
<RegistrationPage />
//... //...
</section>
</Suspense>
);
}

The major advantage to lazy load a single component is to separate components that are Javascript heavy, like rich text editor or big visualizations, by doing so we can separate a large chunk of code that might not even be used by the user at that time.

Route Based Code Splitting

A safe choice to start code splitting with would be to split at the route level. When moving between pages the user is already used to the page taking some time to load. Splitting at the route level will help to evenly split your bundle and won’t harm the user experience.

The following example will lazy load two components that holds different pages, the Home component and the About component.

Copy
import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const Home = lazy(() => import("./routes/Home"));
const About = lazy(() => import("./routes/About"));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);

When you navigate the application you can open the developer tools and actually see the browser loading each bundle chunk as you move between pages.

Important note, use Suspense as a containing element for the rest of your components, even the ones that are not lazy loaded. Components outside the Suspense might not load in earlier versions of react.

Code Split Based On Related Component Groups

Let’s say we have an e-commerce website containing the following pages:

Group A – Cart, Checkout , Payment & Thank you page.

Group B – Profile, Contact & Purchase history page.

Group C – Registration, Login, Home page & Product page.

You can divide your users based on their online behavior.

If the majority of your users only login to the website, scroll through the home page a bit, and view a few product pages, then the smarter choice will be to cater your initially loaded bundle to this use case, therefore giving a better user experience to the majority of your users.

By code splitting based on component groups, we can reach a low bundle size and give the maximum functionality to our users. This will definitely decrease your initial load time and improve overall performance. If by any chance the user actually needs the profile page / contact page then he will be shown a loading indicator for a short while without breaking his overall user experience.

GROUPPING OUR ROUTES

Copy
// GroupC.jsx
import React from "react";
import { Route } from "react-router-dom";
// Importing all the components of this group.
import HomePage from "./components/HomePage";
import LoginPage from "./components/LoginPage";
import ProductPage from "./components/ProductPage";
import RegistrationPage from "./components/RegistrationPage";
const GrouppedRoutes = () => {
return (
<>
<Route exact path="/login" component={LoginPage} />
<Route exact path="/" component={HomePage} />
<Route exact path="/product" component={ProductPage} />
<Route exact path="/register" component={RegistrationPage} />
</>
);
};
export default GrouppedRoutes;

LAZY LOADING A GROUP

Copy
import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
const GroupA = lazy(() => import("./routes/GroupA"));
const GroupB = lazy(() => import("./routes/GroupB"));
const GroupC = lazy(() => import("./routes/GroupC"));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<GroupA />
<GroupB />
<GroupC />
</Switch>
</Suspense>
</Router>
);

I’ve used a bad naming convention so it will be easier to understand the concept of grouping related components. By no means you should name your groups in the same way, it has no meaning and we don’t know what components are in which group.

The example above shows us how we can split our components into related groups and lazy load each group separately.

The above examples were the main best practices for code splitting.

Preloading Imports

After we’ve taken the time to learn about how to use code splitting, it is a good idea to know how to preload the separated chunks / bundles on demand.

A simple example could be that you are dynamically importing a modal, and to avoid the loading indicator you might want to preload the modal when the user hovers the link that opens it.

Another example might be that after the initial bundle was downloaded, you would like to start preloading the other separated bundles quietly in the background, without the user noticing.

To do so let’s look at the following code:

Copy
const AdvancedLazy = (importStatement) => {
const LazyComponent = React.lazy(importStatement);
LazyComponent.preload = importStatement;
return LazyComponent;
};

As you can see I’ve created a function named AdvancedLazy, it accepts a dynamic import and returns a component that will be lazy loaded.

The only difference is I’ve added a preload property to the component, we can then call the import function on any event we decide to preload that component.

Let’s use the example where we want to preload other separated bundles in the background as I’ve talked about above.

The following code is lazy loading the product page, which I decided it should not be loaded as part of the initially loaded bundle. Now to decrease waiting time and improve user experience a good use case might be that you want to preload the product page in the background when the application is mounted.

Copy
import Login from "./Login";
import HomePage from "./HomePage";
import { AdvancedLazy } from "./Utility";
// Lazy loaded Component:
const ProductPage = AdvancedLazy(() => import("./ProductPage"));
// To preload use:
// ProductPage.preload()
export default class App extends React.Component {
componentDidMount() {
ProductPage.preload();
}
render() {
return (
<Router>
<React.Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={HomePage} />
<Route exact path={"/login"} component={Login} />
<Route exact path={"/productPage"} component={ProductPage} />
</Switch>
</React.Suspense>
</Router>
);
}
}

When the application is mounted I’m preloading the product page in the background. You can see the split bundle is being loaded in the background by looking at the Network tab in the developer tools. Now when the user enters the product page, it is already loaded therefore there will be no loading indicator showing up.

Currently I have not named the separated bundle, it’s name will be generated by Webpack, to name the bundle read the following section.

Naming Bundle Chunks With Webpack's Magic Comments

After splitting our bundle webpack will generate a random name for each bundle. It will look some what like this:

Copy
130.59 KB build/static/js/main.z67x322b.js
25.64 KB build/static/js/3.367f1bdd.chunk.js
17.28 KB build/static/js/2.hfd8367f.chunk.js
13.88 KB build/static/js/1.mdcfd383.chunk.js

Those names won’t allow us to know which component we split into which bundle. That’s why naming is so important, when you are able to recognize each bundle you might decide that some bundles should be removed, merged or that a certain component might not be worth code splitting.

“Magic Comments” are comments that webpack recognizes at build time. we will use the following magic comment, webpackChunkName in order to name our chunked bundles:

Copy
import(/* webpackChunkName: "Login" */ "./components/Login");

After naming each chunk we can expect something like this:

Copy
130.59 KB build/static/js/main.z67x322b.js
25.64 KB build/static/js/HomePage.367f1bdd.chunk.js
17.28 KB build/static/js/ProductPage.hfd8367f.chunk.js
13.88 KB build/static/js/Login.mdcfd383.chunk.js

To find out more about magic comments, go to Webpack’s docs:

Webpack Docs – Magic Comments

Code Coverage

Code coverage shows us how much of the code we sent to the user is actually being used in a particular page, this code even if its not being used at the moment is still being parsed and therefore takes more time and resources.

To see the code coverage go to the Developer Tools, click CTRL+SHIFT+P and write “Coverage”, then click the reload button and it will record the loaded files.

Here is an example of code coverage for an article from hackernoon:

Code Coverage Hackernoon

The red area shows unused code inside each downloaded Javascript file, and the blue area shows used amount of code. Check out your bundle code usage and try keeping unused code to a minimum by code splitting and removing redundant and unused code.

Importing Routes Dynamically

Because the dynamic import is using a function you will probably want to pass on arguments to your imports and get different chunks dynamically.

Copy
const selectTheme = (theme) => import(`src/components/themes/${theme}`);

The idea is solid but you must understand that the “dynamic import” is not really as dynamic as you might have thought. All the dynamic imports are parsed by Webpack at build time, therefore nothing is really dynamic like your used to.

Now when Webpack sees a “dynamic import” that receives an argument, at build time it will parse all available files at that folder into separate bundles, so it can “dynamically” load it at run time.

*  *  *

This article went over a lot of subjects, I hope that code splitting will now become a part of your skill set when you tackle performance issues.

© 2020-present Sagi Liba. All Rights Reserved