Following based practices of modern Web development we minimize and bundle JS in a single file to the client. As a next step we split vendor and application code into separate bundles to achieve better client caching. Nevertheless, most of the time we load extra bits of the code that aren’t required for the current page.

Definition of the code-splitting by Addy Osmani:

Code-splitting is one answer to the problem of monolithic bundles. It’s the idea that by defining split-points in your code, it can be split into different files that are lazy loaded on demand. This improves startup time and help us get to being interactive sooner.

I found an excellent introduction into the topic in his article:

Progressive Web Apps with React.js: Part 2 — Page Load Performance
Part 2 of a new series walking through tips for shipping mobile web apps optimized using Lighthouse. This issue, we’ll…medium.com

I recommend to read the whole series about Progressive Web Apps.

The application feature set was constantly growing. At that time the size of the JS bundle was about 1.1 MB of minified unzipped code. With the help of Webpack bundle analyzer, I found out that some of bigger libraries like redux-form and react-modal are used on the few pages but loaded everywhere. I decided to explore the possibility of using Webpack code-splitting to minimize the initial size of the JavaScript we load.

I will describe the idea behind the implementation of code-splitting and learned lessons along the way.

Code-splitting by routes

The common approach to code-splitting using Webpack and React based application is to utilize Webpack's require.ensure and React Router dynamic routing:

Using this approach components code will be loaded asynchronously and only for requested routes.

You will need a polyfill for require.ensure to run SSR (server-side rendering)

Code-splitting by component

Unfortunately the approach using react-router dynamic routing is not always applicable. This was a case with our application. Although we use react-router, the decision about which React components should be rendered on the page is driven by a modules tree represented in the following structure:

{
 "modules": {
  "content_type": "page",
  "items": [{
    "content_type": "section",
    "items": [{
     "content_type": "block",
     "items": [],
     "content": {
      "size": 12
     },
    }],
    "content": {
      ...
    },
    "locale": "en"
   }, {
    "content_type": "carousel",
    "items": [{
      "content_type": "carousel_item",
      "content": {
        ...
      },
     },
     ...
    ],
   },
   ...
   {
    "content_type": "section",
    "items": [],
    "content": {
     ...
    },
   }
  ],
  }
}

Then in our code we have following mapping factory:

And finally EntryFactory component glues everything together:

In the code above, it traverses modules tree and calls render on each component returned from the contentTypes mapping file.

The initial plan was the following:

  • add require.ensure split points inside the contentTypes factory
  • EntryFactory will traverse modules structure and initiate necessary requests for the additional code chunks to be loaded from the server.
  • Until the code is loaded we will render empty content for some components and rerender when download will be completed
  • All possible artifacts due to async loading nature of code chunks will be compensated with SSR payload.

After some investigation I stick with a simple react-promise component and this changes in the contentTypes factory:

For calendar component Webpack will generate separate calendar.js chunk module. We return unresolved promise on the first call to render calendar content type. As soon as the file is downloaded we resolve the promise and cache result in the promises object.

The changes in the EntryFactory to support promise rendering:

The open questions:

  • current implementation broke server-side rendering
  • returning promises from the render function felt wrong

On the positive side of the things:

  • ultimate flexibility with split points, you can group chunk modules whatever you like. In our case, we ended up with 4 additional files each containing from 1 to 10 components.
  • code changes touched only 2 files, no code changes in the components itself

Fixing SSR

After some testing, I found out that ReactDOMServer.renderToString(c) returned empty markup for any component which required chunk loading.

Apparently, there is a long-standing open issue on Github devoted to exactly this topic:

Support asynchronous server rendering (waiting for data before rendering) · Issue #1739 · facebook…
It would seriously ease the process of building something isomorphic if componentWillMount could return a promise and…github.com

In summary, you should preload any async data upfront before rendering on the server.

The quote from the core team member:

We don’t currently support per-component async fetching. We’d like to add it. This issue tracks our progress, though no one is actively working on it and it will take major restructuring so it will be a while.

So, I wrote a small module which did preloading of all code chunks before rendering components tree:

You can use it like:

fetchDependencies(getInitialState(), store).then(() => render());

Minor changes in the contentTypes factory to expose modules promises and cache resolved components in modules object:

With modulesResolver in place, EntryFactory returned to its original pre react-promise version.

Prevent FOUC

By default additional files required with Webpack's require.ensure contain embedded styles. That results into Flash of unstyled content on the initial load.

FOUT on calendar page

To include all styles into combined css file I simply pass {allChunks: true} into ExtractTextPlugin:

Redux async reducers

In this chapter I will explain how to load Redux reducers asynchronously using popular library redux-form as an example. Webpack bundle analyses highlighted a heavy dependency of redux-form as a good candidate for chunk loading. The issue is with a way it integrates into an application:

The import directive will always ensure to include library into js bundle.

I found a nice solution from Dan Abramov how to dynamically load reducers:

How to dynamically load reducers for code splitting in a Redux application?
This is not a full answer but should help you get started. Note that I'm not throwing away old reducers-I'm just adding…stackoverflow.com

It's almost 1 to 1 implementation provided in the original answer. I provide it to show how generic the approach is to apply with require.ensure.

Measurements

Any performance related changes won't be justified unless we compare metrics before and after changes. I used pwmetrics CLI tool powered by Lighthouse. Every result is a median of 5 runs. I measured 4 most visited type of pages:

Home page before
Home page after
Calendar page before
Calendar page after
Auction page before
Auction page after
Lot page before

There is an open issue which prevented to run metric multiple times on Lot page, so only single run metric is available.

Lot page after

On average Time to Interactive was improved by 650 ms with the most improvement on the pages where least amount of Javascript is required to load. Another takeaway is that chunk loading has a positive improvement on every page of the website. This is awesome!

Conclusions

It was a valuable exercise with positive outcomes on the website performance. It allowed me to have a better understanding of the technology stack: React SSR, Redux, Webpack. Most importantly this makes a good foundation of growing web application without sacrificing client side performance. I hope my overview will inspire others to have a look on code-splitting in their projects.

Learned lessons:

Code repo to play with.