Faster Web App loading with Webpack code splitting

25 Apr 2016

The problem

If you're developing a Single Page Application, you're probably familiar with bundling tools like Webpack or Browserify, which provide useful build tools for your font-end codebase and dependencies. With the default setup these tools will build a dependency tree based on require() (or import with ES6 syntax) calls and bundle all dependencies into a single script asset. However, this approach has its limits. One for instance is the size of the output asset. If your app becomes big enough, including multiple 3rd party dependencies, you may quickly end up with a heavy application script that takes a siginificant amount of time to load before user can actually use it. This in effect may leave an impression of the website being slow, not responsive or even broken. That's especially the case if you're developing a single page app, with the application script being in charge of whole app logic, markup rendering, etc.

Luckily, Webpack provides tools for splitting application assets into chunks that can be loaded on demand.

Chunk your app

I recommend checking out official Webpack docs and the GitHub README section for more details about available configuration options. The general idea is to define which dependencies will be loaded on demand (asynchronously) and which will remain in the main root asset. There is a special variant of the CommonJS require call - require.ensure(dependencies, callback). It provides a callback function, which executes once the dependency has been loaded. The Webpack's bundle process checks which dependencies are required with require.ensure and put them in the on-demand chunk. (However If a dependency was also required using the standard require call, it will go to the main chunk and not to the on-demand one). At the moment there was no ES6 import counterpart for require.ensure(dependencies, callback) in the Babel transpiler.

As a brief illustration I prepared a small demo app!

Let's say we're building an online T-shirt store and we want our users to be able to put their custom design on the clothing as part of the order process.

How to run the app:

To make our canvas operations easier we'll use fabric.js library, which provides a higher-level API for managing HTML5 Canvas objects. With a piece of code you can setup a canvas where users can upload an image and edit it interactively (drag, scale, rotate, etc) and save the output.

Notice, that the application won't need the fabric dependency until user actually navigates to the design/checkout view. So that's a good candidate for putting it in the "on-demand" chunk. In the sample repo there are two versions of the TshirtEditor class, one is loading fabric as any other dependency and the second one is loading it asynchronously using require.ensure.

Let's see how's the dependency imported in each case:

TshirtEditor.js:

//...
import {fabric} from "../bower_components/fabric.js";

export default class TshirtEditor {
  //...
  toggleEditor() {
    //...
    this.renderCanvas();
  }
  renderCanvas() {
    // fabric canvas setup..
  }
};

TshirtEditor_split.js:

//... (no import/require for fabric here)
export default class TshirtEditor {
  //...
  toggleEditor() {
    //...
    require.ensure(['../bower_components/fabric.js/'], (require) => {
          var {fabric} = require('../bower_components/fabric.js/');
          this.renderCanvas();
      }, 'tshirt-editor');
  }
  renderCanvas() {
    // fabric canvas setup..
  }
};

With code-splitting, we'll initialize our canvas inside the provided callback method, which is called once the dependency has been loaded. In this example I used a named chunk (tshirt-editor) but that is optional.

Looking at DevTools, the initial app script load looks like this (18.1 KB loaded):

...and only after user visits the T-shirt view, the application requests the editor chunk (888 KB unminified):

Task accomplished - we managed to keep the main app script thin and improved load times :)

When to use Code splitting?

I think good idea is to first look for dependencies that are used in a specific part of your app and to require them on-demand. For instance if you are showing a volume of sales summary, using a data grid view component in the user profile, let that data grid dependency go to a separate chunk and get loaded only when user goes to that specific view.

Another common usage is splitting your source code and vendor scripts into separate chunks, as described here. It all depends on your particular setup.

comments powered by Disqus