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.
The open source library
provides a React-friendly API for code splitting, and lets you add breakpoints with just a few lines of code. If you're using react-loadable
, Webpack automatically takes care of splitting the bundle and loading chunks on demand under the hood.create-react-app
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.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.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.
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
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
.Here are steps for achieving effective code splitting in React applications:
react-loadable
to achieve component-based code splitting and load Javascript bundles for different parts of the application on demand.import("./index").then
trick./* webpackChunkName: xxx */
and optimize bundle sizes so that they are neither too small nor too large.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:
import
: https://developers.google.com/web/updates/2017/11/dynamic-importreact-loadable
: https://github.com/jamiebuilds/react-loadable