Code Splitting

From the blog:
ajv - The Fastest JSON Schema Validator - Interview with Evgeny Poberezkin

Web applications have the tendency to grow big as features are developed. This can be problematic especially for mobile usage. The longer it takes for your application to load, the more frustrating it is to the user. This problem is amplified in a mobile environment where the connections can be slow.

Even though splitting our bundles can help a notch, they are not the only solution and you may still end up having to download a lot of data. Fortunately, it is possible to do better thanks to a technique known as code splitting. It allows us to load code lazily as we need it.

Incidentally, it is possible to implement Google's PRPL pattern using lazy loading. PRPL (Push, Render, Pre-cache, Lazy-load) has been designed mobile web in mind and can be implemented using webpack.

Bundle with a dynamically-loaded normal chunk

Code Splitting Formats#

Code splitting can be done in two primary ways in webpack: through a dynamic import or require.ensure syntax. We'll be using the former in this demo. The syntax isn't in the official specification yet so it will require minor tweaks especially at ESLint and Babel too if you are using that.

Dynamic import#

Dynamic imports look like this:

import('./module').then((module) => {...}).catch((error) => {...});

The Promise based interface allows composition and you could load multiple resources in parallel if wanted. Promise.all would work for that like this:

Promise.all([
  import('lunr'),
  import('../search_index.json'),
]).then(([lunr, search]) => {
  return {
    index: lunr.Index.load(search.index),
    lines: search.lines,
  };
});

It is important to note that this will create separate chunks to request. If you wanted only one, you would have to define an intermediate module to import.

require.ensure#

require.ensure provides an alternate way:

require.ensure(
  // Modules to load, but not execute yet
  ['./load-earlier'],
  () => {
    const loadEarlier = require('./load-earlier');

    // Load later on demand and include to the same chunk
    const module1 = require('./module1');
    const module2 = require('./module2');

    ...
  }
);

As you can see, require.ensure definition is more powerful. The gotcha is that it doesn't support error handling. Often you can achieve what you want through a dynamic import, but it's good to know this form exists as well.

require.ensure relies on Promises internally. If you use require.ensure with older browsers, remember to shim Promise.

require.include#

The example above could be rewritten using a webpack specific function known as require.include:

require.ensure(
  [],
  () => {
    require.include('./load-earlier');

    const loadEarlier = require('./load-earlier');

    // Load later on demand and include to the same chunk
    const module1 = require('./module1');
    const module2 = require('./module2');

    ...
  }
);

If you had nested require.ensure definitions, you could pull a module to the parent chunk using either syntax.

The formats respect output.publicPath option. You can also use output.chunkFilename to shape where they output. Example: chunkFilename: 'scripts/[name].js'.

Setting Up Code Splitting#

To demonstrate the idea of code splitting, we should pick up one of the formats above and integrate it to our project. Dynamic import is enough. Before we can implement the webpack side, ESLint needs a slight tweak.

Tweaking ESLint#

Given ESLint supports only standard ES6 out of the box, it requires some tweaking to work with dynamic import. Install babel-eslint parser first:

npm i babel-eslint -D

Tweak ESLint configuration as follows:

.eslintrc.js

module.exports = {
  "env": {
    "browser": true,
    "commonjs": true,
    "es6": true,
    "node": true,
  },
  "extends": "eslint:recommended",
"parser": "babel-eslint",
"parserOptions": { "sourceType": "module",
"allowImportExportEverywhere": true,
}, ... }

After these changes, ESLint won't complain if we write import in the middle of our code.

Defining a Split Point Using a Dynamic import#

A simple way to illustrate the idea might be to set up a module that contains a string that will replace the text of our demo button. Set up a file as follows:

app/lazy.js

export default 'Hello from lazy';

In practice, you could have a lot more code here and additional split points. This is a good place to extend the demonstration.

We also need to point the application to this file so it knows to load it. A simple way to do this is to bind the loading process to click. Whenever the user happens to click the button, we'll trigger the loading process and replace the button content.

app/component.js

export default function () {
  const element = document.createElement('h1');

  element.className = 'pure-button';
  element.innerHTML = 'Hello world';
element.onclick = () => { import('./lazy').then((lazy) => { element.textContent = lazy.default; }).catch((err) => { console.error(err); }); };
return element; }

If you open up the application (npm start) and click the button, you should see the new text in the button.

Lazy loaded button content

Perhaps the more interesting thing is to see what the build result looks like. If you run npm run build, you should see something like this:

Hash: 230ee0afd5597af17cb1
Version: webpack 2.2.0
Time: 1901ms
        Asset       Size  Chunks             Chunk Names
         0.js  314 bytes       0  [emitted]
       app.js    2.32 kB       1  [emitted]  app
    vendor.js     141 kB       2  [emitted]  vendor
      app.css     2.2 kB       1  [emitted]  app
     0.js.map  277 bytes       0  [emitted]
   app.js.map    1.95 kB       1  [emitted]  app
  app.css.map   84 bytes       1  [emitted]  app
vendor.js.map     167 kB       2  [emitted]  vendor
   index.html  274 bytes          [emitted]
   [0] ./~/process/browser.js 5.3 kB {2} [built]
   [3] ./~/react/lib/ReactElement.js 11.2 kB {2} [built]
   [7] ./~/react/react.js 56 bytes {2} [built]
...

That 0.js is our split point. Examining the file reveals that webpack has wrapped the code in a webpackJsonp block and processed the code bit.

Defining a Split Point Using require.ensure#

It is possible to achieve the same with require.ensure. Consider the full example below:

export default function () {
  const element = document.createElement('h1');

  element.className = 'pure-button';
  element.innerHTML = 'Hello world';
  element.onclick = () => {
    require.ensure([], (require) => {
      element.textContent = require('./lazy').default;
    });
  };

  return element;
}
bundle-loader gives similar results, but through a loader interface.

Dynamic Loading with require.context#

Beyond the variants above, there's another type of require that you should be aware of. require.context is a general form of the above.

Let's say you are writing a static site generator on top of webpack. You could model your site contents within a directory structure. At the simplest level, you could have just ./pages/ directory which would contain Markdown files.

Each of these files would have a YAML frontmatter for their metadata. The url of each page could be determined based on the filename. This is enough information to map the directory as a site. Code-wise we would end up with a statement like this somewhere:

// Process pages through `yaml-frontmatter-loader` and `json-loader`.
// The first one extracts the frontmatter and the body and the latter
// converts it into a JSON structure we can use later. Markdown hasn't
// been processed yet.
const req = require.context(
  'json-loader!yaml-frontmatter-loader!./pages',
  true, // Load files recursively. Pass false to skip recursion.
  /^\.\/.*\.md$/ // Match files ending with .md.
);

require.context returns us a function to require against. It also knows its module id and it provides a keys() method for figuring out the contents of the context. To give you a better example, consider the code below:

req.keys(); // ['./demo.md', './another-demo.md']

req.id; // 42

// {title: 'Demo', __content: '# Demo page\nDemo content\n\n'}
const demoPage = req('./demo.md');

This information is enough for generating an entire site. And this is exactly what I've done with Antwar. You can find a more elaborate example in that static site generator.

The technique can be useful for other purposes, such as testing or adding files for webpack to watch. In that case, you would set up a require.context within a file which you then point to through a webpack entry.

Note that webpack will also turn statements written in the form require('./pages/' + pageName + '.md') into the require.context format!

Combining Multiple require.contexts#

Sometimes you might need to combine multiple separate require.contexts into one. This can be done by wrapping them behind a similar API like this:

const { concat, uniq } from 'lodash';

function combineContexts(...contexts) {
  function webpackContext(req) {
    // Find the first match and execute
    const matches = contexts.map(
      context => context.keys().indexOf(req) >= 0 && context
    ).filter(a => a);

    return matches[0] && matches[0](req);
  }
  webpackContext.keys = () => uniq(
    concat.apply(
      null,
      contexts.map(
        context => context.keys()
      )
    )
  );

  return webpackContext;
}

Dynamic Paths with a Dynamic import#

The same idea works with dynamic import. Instead of passing an absolute path, you can pass a partial one. Webpack will set up a context internally. Here's a brief example:

// Set up a target or derive this somehow
const target = 'demo.json';

// Elsewhere in code
import(`indexes/${target}`).then(...).catch(...);

Dealing with Dynamic Paths#

Given the approaches discussed here rely on static analysis and webpack has to find the files in question, it doesn't work for every possible case. If the files you need are on another server or have to be accessed through a specific end-point, then webpack isn't enough.

Consider using browser-side loaders like $script.js or little-loader on top of webpack in this case.

Conclusion#

Code splitting is one of those features that allows you to push your application a notch further. You can load code when you need it. This gives faster initial load times and helps to improve user experience especially in a mobile context where bandwidth is limited.

It comes with some extra work as you must figure out what's possible to split. Often, you find good split points within a router. Or you may notice that specific functionality is required only when specific feature is used. Charting is a good example of this.

Just applying import or require.ensure alone can be very effective. require.context has more limited possibilities, but it's a powerful tool especially for tool developers.

There's a more complete example of how to use the code splitting technique in the Searching with React appendix. You will see how to set up a static site index that's loaded when the user searches information.
Previous chapterSplitting Bundles

This book is available through Leanpub. By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of Webpack.

Need help?