paint-brush
Effective Code Splitting in React: A Practical Guideby@aakashns
32,874 reads
32,874 reads

Effective Code Splitting in React: A Practical Guide

by Aakash N SApril 9th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Large bundle sizes and slow startup is a common problem faced by single-page applications (SPAs), since they typically download all the JavaScript required for every single page of the application right at the start, before rendering a single pixel.

Company Mentioned

Mention Thumbnail
featured image - Effective Code Splitting in React: A Practical Guide
Aakash N S HackerNoon profile picture

Large bundle sizes and slow startup is a common problem faced by single-page applications (SPAs), since they typically download all the JavaScript required for every single page of the application right at the start, before rendering a single pixel.

A simple way to solve this problem is to use code-splitting i.e. breaking down the application’s JavaScript into small, modular bundles called chunks, which can be loaded on-demand when a particular feature is accessed. The goal is to keep individual chunks under 100–150 KB, so that the application becomes interactive within 4–5 seconds, even on poor networks.

Component-based code splitting

The open source library

react-loadable
provides a React-friendly API for code splitting, and lets you add breakpoints with just a few lines of code. If you're using
create-react-app
, Webpack automatically takes care of splitting the bundle and loading chunks on demand under the hood.

Here’s how it works: suppose we want to load and render the component

SettingsPage
on demand, when the user clicks on a particular or navigates to a particular route. All we need to is wrap it using
react-loadable
as follows:

import Loadable from "react-loadable";
import Loading from "./Loading";

const AsyncSettingsPage = Loadable({
  loader: () => import("./SettingsPage"),
  loading: Loading
});

export { AsyncSettingsPage };

Now we can use

AsyncSettingsPage
just like a normal React component. The module
SettingsPage.j
s and its dependencies are no longer a part of the main JavaScript bundle and are loaded asynchronously when
AsyncSettingsPage
is rendered for the first time.

While the chunk is loading, the component

Loadin
g is rendered in its place. Here’s a sample implementation of
Loading
:

import React from "react";

const Loading = props => {
  if (props.error) {
    return <div>Error!</div>;
  } else {
    return <div>Loading...</div>;
  }
};

export default Loading;

The prop

error
is set to a non-null value if the chunk fails to load.

Chunking multiple components together

There are some cases where simple component-based splitting may not be enough. For instance, you may have a set of components that are almost always used together in a several different features. In such a case, it makes sense to have a single chunk which contains the entire set of related components.

Here’s how we might normally export a set of related components:

import ItemListHeader from "./ItemListHeader";
import ItemListFilters from "./ItemListFilters";
import ItemListTable from "./ItemListTable";
export {
  ItemListHeader,
  ItemListFilters,
  ItemListTable
};

Assuming the above code is in the file

item-list/index.js
, we can create another file
item-list/async.js
with the following contents:

import React from "react";
import Loadable from "react-loadable";
import Loading from "./Loading";

const AsyncItemListHeader = Loadable({
  loader: () => import("./index").then(m => m.ItemListHeader),
  loading: Loading
});

const AsyncItemListFilters = Loadable({
  loader: () => import("./index").then(m => m.ItemListFilters),
  loading: Loading
});

const AsyncItemListTable = Loadable({
  loader: () => import("./index").then(m => m.ItemListTable),
  loading: Loading
});

export {
  AsyncItemListHeader,
  AsyncItemListFilters,
  AsyncItemListTable,
};

The key change here is in the dynamic

import
: instead of importing a single component, we are importing all of
index.js
and extracting the required component in the promise callback.

Chunk naming and optimization

When we build application for production after implementing code splitting, we get many chunks of Javascript that look like this:

File sizes after gzip:  396.71 KB              build/static/js/main.3a8842c0.js
  178.51 KB              build/static/css/main.e32b4522.css
  68.31 KB               build/static/js/6.af93367f.chunk.js
  44.34 KB               build/static/js/2.6a7f1417.chunk.js
  23.61 KB               build/static/js/1.bdfdcd83.chunk.js
  22.24 KB               build/static/js/3.d9e4ee99.chunk.js
  19.29 KB               build/static/js/4.a66b3cdb.chunk.js
  17.1 KB                build/static/js/5.f1ce26f7.chunk.js
  7.63 KB                build/static/js/8.2e807534.chunk.js
  6.71 KB                build/static/js/9.409015da.chunk.js
  5.09 KB                build/static/js/7.1b95d8e8.chunk.js
  1.71 KB                build/static/js/0.6bea2af7.chunk.js
  1 KB                   build/static/js/10.ce9f2434.chunk.js

After looking at this output, we might want to remove some of the last few chunks since they’re really small. But we don’t know which split is causing which chunk to be created. This is where chunk naming can be helpful.

We can use a magic comment inside the

impor
t that tells Webpack to use the given name for a specific chunk:

const AsyncSettingsPage = Loadable({
  loader: () => import("./SettingsPage" /* webpackChunkName: "settings" */),
  loading: Loading
});

Once all the chunks are named, we can identify the splits that lead to smaller chunks:

File sizes after gzip:  312.09 KB  build/static/js/main.491eaaf4.js
  181 KB      build/static/css/main.ac06cedb.css
  68.88 KB   build/static/js/settings.1525d075.chunk.js
  45.08 KB   build/static/js/alerts.0f5ad4d6.chunk.js
  23.62 KB   build/static/js/profile.199c7f90.chunk.js
  22.24 KB   build/static/js/history.07ccea31.chunk.js
  19.3 KB    build/static/js/actions.903378a5.chunk.js
  8.87 KB    build/static/js/events.f540de3a.chunk.js
  7.62 KB    build/static/js/colors.89aa1e6f.chunk.js
  6.7 KB     build/static/js/posts.929f04fc.chunk.js
  5.1 KB     build/static/js/post-details.6c133f77.chunk.js
  1.71 KB    build/static/js/friend-list.be516e45.chunk.js
  1.01 KB    build/static/js/edit-avatar.33a4ff21.chunk.js

At this point, we can choose to remove or combine some of the smaller chunks (< 20–30 KB in size), since the overhead of loading a 5 KB chunk might be higher than combining it with one of the larger chunks. Play around with different splits and see what works best for you.

Analyzing the Bundle Size

Source map explorer analyzes JavaScript bundles using the source maps. This helps you understand where code bloat is coming from. To add Source map explorer to a Create React App project, run the following command:

npm install --save source-map-explorer

Then in

package.json
, add the following line to
scripts
:

"scripts": {
    "analyze": "source-map-explorer build/static/js/main.*",

Then to analyze the bundle run the production build then run the analyze script.

npm run build
npm run analyze

Source map explorer

Look for the largest contributors to the bundle size as possible candidates for code-splitting. Also consider removing or pruning large dependencies from

node_modules
.

Summary

Here are steps for achieving effective code splitting in React applications:

  • Use
    react-loadable
    to achieve component-based code splitting and load Javascript bundles for different parts of the application on demand.
  • Chunk multiple components that are frequently used together into a single file using the
    import("./index").then
    trick.
  • Name your chunk using the magic comment
    /* webpackChunkName: xxx */
    and optimize bundle sizes so that they are neither too small nor too large.
  • Use
    source-map-explorer
    to identify possible candidates for code splitting.

I’ve skipped over many details to keep this article short and focus on the practical aspects of code splitting. Following are some good places to learn more about the topic: