Categories
Code Life

Pro Tips For Designing Robust React Components Part II: Bundle Size

A few weeks ago, I shared some tips for improving React app performance. Let’s see how another metric can be improved - the bundle size.

Why is bundle size important?

Because that affects how quickly your app will load when a user opens your page. It is vital because many users will probably be connecting from dodgy 3G or wireless connections, where the speed is slow, and thus a small bundle size is essential so that users don’t leave your site. Users tend to leave a site if a page takes longer than 3 seconds to load. The 2-second threshold is the “danger zone” where most users expect the app to be fully loaded within that time and begin to get impatient if it isn’t.

Granted, React app loading is not symmetrical to page loading - generally, you can load a bunch of HTML and CSS much faster than a React.js bundle file. However, load time is still important even though you have a slightly longer amount of time to render the app. So while users will forgive you for your app taking 10 seconds to render, the same cannot be said for 60 seconds, 45, and possibly even 30 seconds.

Nobody’s expecting you to render a React app in 2 seconds, though if you can, then your team should throw a pizza and beer celebration. For the rest of you, here are some techniques to shrink the bundle size.

Split your bundles into smaller ones

This is a very powerful technique to make the app load faster because instead of one large bundle, it is now a bunch of smaller ones that Webpack can load on-demand. So, you can package your app’s dashboard as a bundle that loads immediately and delay the loading of bundles representing other auxiliary pages. I imagine this is what Facebook, Instagram, and others use to keep the load time of their main sites - which are written in React - manageable.

Splitting bundles is a feature available since Webpack 4. Apps made nowadays likely are not building using Webpack 3 or lower, so there should be no worries about upgrading to a slightly incompatible version.

How does code-splitting work?

The Webpack documentation gives us 3 methods to implement code splitting. The first one uses entry points using entry configuration lines in the Webpack config. This basically means that each component tree you want to split off has some ancestor component in a specific file referenced in the Webpack configuration. The entire tree is bundled down to a single bundle.

This is how you use entry to define different bundles Webpack needs to make:

const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};

You must include dependOn: 'shared' for all the bundles, and then list any external libraries you’re importing as dependencies and the file name of each component used by the multiple component trees. Otherwise, the shared dependencies are duplicated in both bundles and defeat the purpose of code splitting. The lodash dependency in this example will occupy more than 500KB in each of the created bundles without shared dependencies.

Of course, it is usually not feasible to place all your shared components in one file. Whatever you write in the dependOn: the directive will have a key right below the entry object, such as shared in this example, and is an array of strings if a bundle has multiple dependencies. Creating multiple dependOn names for different bundles allow you to define multiple shared entry points whose paths reflect the structure of your React app.

Refactor long lists of content as separate XHR calls

If you have any long arrays of text strings in your React app, these might be weighing down the load time. Try to make an API endpoint to supply this data, then use node-fetch to retrieve it at runtime, using a progress indicator as a placeholder while the request completes. You can use this alongside code splitting to fetch the content before additional bundles are loaded, which reduces the time to render before a user can interact with the app.

The react-window module has been designed to fetch long lists of content. It has an additional performance optimization, however. Instead of fetching the entire list, it only fetches the amount that fits in the viewport and then issues a DOM update. This is useful if, for some reason, your list, along with all its properties, is several megabytes large. It happens sometimes.

Additionally, you might be able to set up your API endpoint to prefetch the request, which will make the server cache the response by the time you’re ready to issue the actual API call. In some cases, this can speed up the time it takes to fetch long lists of content.

Use tree-shaking

Tree-shaking is a process that eliminates dead code from bundles. For this to work, you must only import the functions you need from modules (i.e., don’t import the whole thing), and you have to place "sideEffects": false in your package.json on the same level as the name property. You can also add it in the Webpack configuration file under the rules property object.

A side effect is any module that, when imported, runs some background function in addition to importing items from the module. Webpack wants to make sure that removing unused functions from the bundle does not accidentally inhibit important code from running. If there are such modules, you should include their file names as an array of strings in the sideEffects property and Webpack will keep those in the bundle.

Note that for this to work, you have to use ES2015 import syntax in your files.

Use service workers

Applicable to all kinds of web apps, not React apps per se.

A service worker is a Javascript file that a page deploys in the background. It “installs” this file by caching all files specified in the “install” event listener. Then, it communicates with the page by sending a `window.postMessage()“ call, whose data is then intercepted by the “message” event listener on the webpage.

But how does a service worker know which page to communicate with? It turns out that postMessage() also takes an origin parameter that tells the browser which pages it should broadcast the message to. So tabs in a browser window that have the same origin will all receive the message.

So service workers don’t really do one-to-one messaging unless there’s only one matching page. Think of it as a publish-subscribe channel where all open pages of the same origin will get the data received in the message. Remember that an origin is a tuple of hostname or domain name, port number, and protocol (HTTP or HTTPS).

Service workers can improve app performance by caching the files specified at install time and then returning them in the “message” payload to open the page. These files are effectively cached in the browser, so apps can use this method to read items such as CSS files, fonts, and other dependencies defined in the HTML, such as <script> tags. It doesn’t work well for caching bundles (use the Webpack server instead), Also without specifying the origin, you create security holes in your app.

Google Developers has some terrific code samples for service worker events in their documentation. They also have an older tutorial that explains how service workers work.


I hope this post has benefited you in your quest to make your React app faster. If you have any other performance ideas, let me know about them in the comments below.

Cover Image by @bobbinio2112 via Twenty20

By Ali Sherief

Editor-in-chief and serial coder & blogger.