Improve Overall User Experience and Performance With React Code Splitting

by | Aug 21, 2020 | 0 comments

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?

2. 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
import Utils from './UtilityFunctions';

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

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.

 

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.
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:

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.

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
// 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
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>
);

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.

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.

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.

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

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 inorder to name our chunked bundles: 

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

After naming each chunk we can expect something like this:

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

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.

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.

IF YOU GOT ANY VALUE SHARE 😄