Route-Based Code Splitting with Loadable Components and Webpack

Jonathan Schwarz
priceline labs
Published in
7 min readApr 1, 2020
Portions of code, which represent code splitting

Does your React app contain multiple routes, but only a single javascript bundle? This article will show you how you can use Webpack 4 and Loadable Components to employ code splitting and achieve some big performance wins!

What is Code Splitting?

The term code-splitting refers to dividing our code into multiple bundles so that we can be smarter about when we require certain portions of code to be loaded. Let’s use a very meta example and say that we are developing Medium, and we would like to implement code splitting on two of Medium’s most important routes: /priceline-labs and /priceline-labs/:article-id. /priceline-labs contains several components, such as article previews and a hero image, that are not used on /priceline-labs/:article-id.

Hero image and article teaser component
Components only used on /priceline-labs

Similarly, /priceline-labs/:article-id uses components like category tags, social media buttons, and a comment button that we do not need on /priceline-labs.

Tags, social media buttons, and “Write the first response” button
Components only used on /priceline-labs/:article-id

Without code splitting, the user would download a bundle.js file, containing all components that are unique to /priceline-labs, as well as all components that are unique to /priceline-labs/:article-id, regardless of which route the user lands on. With a code-splitting implementation in place, the user will only be served the necessary bundle(s) needed to render their route of choice. Since the browser no longer needs to download and execute as many bytes of javascript, the page will become interactive and visually ready more quickly. The rest of the code needed to render other routes can be downloaded in the background while the user interacts with the app.

How Can We Get Started?

Now that we understand how code splitting can help us, let’s take a look at some tools we can use to set it up. First, it’s important to understand that code splitting is made possible by our bundler, which is Webpack in our case. Any tool we use on top of that is communicating with Webpack to help define how our bundles should be divided. As mentioned in Webpack’s Code Splitting Documentation, there are 3 methods one can use to split code:

• Entry Points: Manually split code using entry configuration.

• Prevent Duplication: Use the SplitChunksPlugin to dedupe and split chunks.

• Dynamic Imports: Split code via inline function calls within modules.

I recommend forgoing Entry Point splitting, since the Webpack documentation also later clarifies, “As a rule of thumb: Use exactly one entry point for each HTML document.” We will be able to achieve sensible code splitting using the other two methods that Webpack makes available to us.

There are several tools we can use to leverage dynamic imports. One of the tools we use must be @babel/plugin-syntax-dynamic-import, which should be added to your babel config to enable support for dynamic imports. This plugin allows us to use import asynchronously to create a callback that will utilize the import when it is ready. Turning the typically synchronous import into a non-blocking asynchronous function enables the browser to execute the remainder of our code in the background, improving performance for the user.

React incorporated code splitting in version 16, by introducing React.lazy, which allows you to store a dynamic import as a React Component. You can accomplish a fair amount of code splitting with React.lazy, but if you want the convenience of built-in preloading capability, and support for server-side rendering, then you should use Loadable Components instead.

Route splitting

Let’s introduce code splitting into our app by splitting our routes. Using Loadable Components, we can achieve this as follows:

The webpackChunkName comment is used to clearly name each bundle, as the automatically generated names are not as informative. If our app does not use server-side rendering (SSR), then the above is all we need to divide our routes into separate bundles! If we want to support SSR, Loadable Components provides handy documentation on how to set this up.

User Experience

Loading the first route

Loadable Components’ SSR setup ensures that the first route the user visits loads smoothly. This is because loadableReady waits until all necessary bundles have loaded before rendering the page, meaning that we don’t have to worry about displaying fallback content, or the user seeing a flash of white in the absence of fallback content, before the first route loads.

A Note on IE11

Loadable Components attaches the async attribute to all <script /> tags by default. This could possibly result in polyfill conflicts in IE11 if some chunks load before your polyfill. To avoid this issue, I recommend disabling async bypassing { async: false } to the getScriptElements or getScriptTags method, depending on which you use.

const scriptElements = extractor.getScriptElements({ async: false })

Loading the second route

To create a smooth user experience, we should preload any bundles needed for the next route as the user interacts with the first route. If we can ensure that our bundles are preloaded before they are needed, then there is no need to pass a loader component as a fallback prop to your loadable components, nor will we need to worry about our users seeing a blank screen as they wait for the bundles to finish downloading. Let’s amend our app.js file to include basic preloading:

Since preloading is only supported on the client-side, we’ve added a preload helper function that will only execute .preload() if it’s available. We cannot know certainly which route a user will land on first and which route they will visit next, so we simply preload every route when our app renders on the client-side, using useEffect (for functional components) or componentDidMount (for class components). Note that .preload() will not reload any bundles that were already loaded, so don’t worry about excluding the current route from the list of routes to preload.

In a more advanced implementation of preloading, we could be more strategic about when we preload certain routes so that we don’t send data to the client that it might not need. For instance, instead of preloading /about when the component mounts, we could execute preload(About) on mouseover of a link.

Vendor Splitting

At this point, we have Loadable Components working and optimized to create a smooth user experience, but there is still one more Webpack code splitting technique we can utilize to improve performance even more. Using the SplitChunksPlugin, we can separate node modules from the rest of our entry bundle by defining cacheGroups. Let’s add the following code to our Webpack config file:

This configuration will create a vendors bundle, which includes all node modules, other than a couple of modules which we are explicitly excluding as they are accounted for in either articleVendors or pricelineLabsVendors. The pricelineLabsVendors bundle has been set up to include only the hero-image-node-module component because it is the only route that requires this dependency. Similarly, the /priceline-labs/:article-id route is the only route that relies on a dependency called social-media-buttons-node-module, so we can specify that this node module should only exist in articleVendors.

So what does this optimization do exactly? Let’s say a user visits the /priceline-labs route first. Loadable Components will:

  1. download our entry bundles: bundle.js (code that is needed to render every route) and vendors.js (node modules that are required in each route).
  2. download route-specific bundles: PricelineLabs and pricelineLabsVendors.

Once the route loads and becomes interactive, we then preload bundles needed to render other pages, such as Articles, articlesVendors, and About.

Performance

So now that we’ve done all that setup, what kinds of performance improvements can we expect to see? This depends on a variety of factors, such as the size of each bundle, how many routes we have, the reliability of the user’s Internet connection, etc. At Priceline, we observed these key findings when we applied all of the recommended code-splitting techniques from this article to one of our applications:

  1. The 95th percentile of users (meaning the slowest connections, excluding the top 5% of slow connections) experienced a 6 second (25%) improvement in terms of how quickly our pages became Visually Ready.
  2. The median (or 50th percentile) experienced a 500 ms (10%) improvement for Visually Ready.
  3. window.DOMContentLoaded occurred after navigation start 2 seconds (25%) sooner for the 95th percentile.
  4. DOMContentLoaded occurred 600 ms (25%) sooner for the median.
  5. Time to First Interactive became 400 ms (33%) faster for the 95th percentile.

Here’s another way of visualizing those performance wins:

A table which restates the performance improvement information above
Performance Improvement after Code Splitting

Why This Matters

The 50th percentile of our users can engage and interact with our site hundreds of milliseconds faster than they could previously. For the 95th percentile, the difference is several seconds. Numerous studies show that those numbers make a huge difference in business success and user satisfaction. In 2019, MachMetrics reported that companies like Walmart and Amazon found that every 100ms of improvement could generate a 1% increase in revenue. Inversely, one second of latency could lead to $1.6 billion lost for Amazon. Every second or tenth of a second you can save really matters for you and your users. That’s why it is absolutely vital to leverage code splitting as much as possible to achieve those savings.

--

--