Code Splitting - Async

This guide documents how to split your bundle into chunks which can be downloaded asynchronously at a later time. For instance, this allows to serve a minimal bootstrap bundle first and to asynchronously additional features later.

webpack supports two similar techniques to achieve this goal: using import() (preferred, ECMAScript proposal) and require.ensure() (legacy, webpack specific).

Dynamic import: import()

Currently, a "function-like" import() module loading syntax proposal is on the way into ECMAScript.

The ES2015 Loader spec defines import() as method to load ES2015 modules dynamically on runtime.

webpack treats import() as a split-point and puts the requested module in a separate chunk. import() takes the module name as argument and returns a Promise: import(name) -> Promise

index.js

function determineDate() {
  import('moment').then(function(moment) {
    console.log(moment().format());
  }).catch(function(err) {
    console.log('Failed to load moment', err);
  });
}

determineDate();

Note that fully dynamic statements, such as import(foo), will fail because webpack requires at least some file location information. This is because foo could potentially be any path to any file in your system or project. The import() must contain at least some information about where the module is located, so bundling can be limited to a specific directory or set of files.

For example, import(`./locale/${language}.json`) will cause every .json file in the ./locale directory to be bundled into the split-point. At run time, when the variable language has been computed, any file like english.json or german.json will be available for consumption. So keep in mind that when using import(), the path must contain some path information or be completely static (as is 'moment' in the example above).

Promise polyfill

import() relies on Promise internally.

If you use import() with older browsers, remember to shim Promise using a polyfill such as es6-promise or promise-polyfill.

In an entry point of your application:

import Es6Promise from 'es6-promise';
Es6Promise.polyfill();
// or
import 'es6-promise/auto';
// or
import Promise from 'promise-polyfill';
if (!window.Promise) {
  window.Promise = Promise;
}
// or ...

Chunk names

Since webpack 2.4.0, chunk names for dynamic imports can be specified using a "magic comment".

import(/* webpackChunkName: "my-chunk-name" */ 'module');

Usage with Babel

If you want to use import with Babel, you'll need to install/add the syntax-dynamic-import plugin while it's still Stage 3 to get around the parser error. When the proposal is added to the spec this won't be necessary anymore.

npm install --save-dev babel-core babel-loader babel-plugin-syntax-dynamic-import babel-preset-es2015
# for this example
npm install --save moment

index-es2015.js

function determineDate() {
  import('moment')
    .then(moment => moment().format('LLLL'))
    .then(str => console.log(str))
    .catch(err => console.log('Failed to load moment', err));
}

determineDate();

webpack.config.js

module.exports = {
  entry: './index-es2015.js',
  output: {
    filename: 'dist.js',
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /(node_modules)/,
      use: [{
        loader: 'babel-loader',
        options: {
          presets: [['es2015', {modules: false}]],
          plugins: ['syntax-dynamic-import']
        }
      }]
    }]
  }
};

Not using the syntax-dynamic-import plugin will fail the build with:

  • Module build failed: SyntaxError: 'import' and 'export' may only appear at the top level, or
  • Module build failed: SyntaxError: Unexpected token, expected {

Usage with Babel and async/await

To use ES2017 async/await with import():

npm install --save-dev babel-plugin-transform-async-to-generator babel-plugin-transform-regenerator babel-plugin-transform-runtime

index-es2017.js

async function determineDate() {
  const moment = await import('moment');
  return moment().format('LLLL');
}

determineDate().then(str => console.log(str));

webpack.config.js

module.exports = {
  entry: './index-es2017.js',
  output: {
    filename: 'dist.js',
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: /(node_modules)/,
      use: [{
        loader: 'babel-loader',
        options: {
          presets: [['es2015', {modules: false}]],
          plugins: [
            'syntax-dynamic-import',
            'transform-async-to-generator',
            'transform-regenerator',
            'transform-runtime'
          ]
        }
      }]
    }]
  }
};

System.import is deprecated

The use of System.import in webpack did not fit the proposed spec, so it was deprecated in v2.1.0-beta.28 in favor of import().

require.ensure()

require.ensure() is specific to webpack and superseded by import().

webpack statically parses for require.ensure() in the code while building. Any module that is referenced as a dependency or require()d within the callback function, will be added to a new chunk. This new chunk is written to an async bundle that is loaded on demand by webpack through jsonp.

The syntax is as follows:

require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
  • dependencies is an array of strings where we can declare all the modules that need to be made available before all the code in the callback function can be executed.
  • callback is a function that webpack will execute once the dependencies are loaded. An implementation of the require function is sent as a parameter to this function. The function body can use this to further require() modules it needs for execution.
  • optional: errorCallback is a function that is executed when webpack fails to load the dependencies.
  • optional: chunkName is a name given to the chunk created by this particular require.ensure(). By passing the same chunkName to various require.ensure() calls, we can combine their code into a single chunk, resulting in only one bundle that the browser must load.

Let's reconsider the dynamic import of moment from the import() section and rewrite it using require.ensure():

index.js

function determineDate() {
  require.ensure([], function(require) {
    var moment = require('moment');
    console.log(moment().format());
  });
}

determineDate();

Running webpack index.js bundle.js generates two files, bundle.js and 0.bundle.js:

bundle.js

// webpack code ...
/***/ (function(module, exports, __webpack_require__) {

function determineDate() {
  __webpack_require__.e/* require.ensure */(0).then((function(require) {
    var moment = __webpack_require__(0);
    console.log(moment().format());
  }).bind(null, __webpack_require__)).catch(__webpack_require__.oe);
}

determineDate();
// webpack code ...

0.bundle.js

webpackJsonp([0],[(function(module, exports, __webpack_require__) {
/* WEBPACK VAR INJECTION */(function(module) {

//! moment.js

})]);

When you add bundle.js in your HTML file and open it in your browser, the 0.bundle.js will be loaded asynchronously by webpack.

publicPath

output.publicPath is an important option when using code-splitting, it is used to tell webpack where to load your bundles on-demand, see the configuration documentation.

Chunk name

require.ensure([], function(require) {
  var foo = require('./module');
}, 'custom-chunk-name');

Use the last argument to require.ensure() in order to specify the name of the chunk.

Error callback

Since webpack 2.4.0, an error callback can be as third argument to require.ensure(). This allows to address errors which occur when dynamically loading the chunk:

require.ensure([], function(require) {
    var foo = require('./module');
}, function(err) {
    console.error('We failed to load chunk: ' + err);
}, 'custom-chunk-name');

Empty Array as Parameter

require.ensure([], function(require){
    require('./a.js');
});

The above code ensures that a split point is created and a.js is bundled separately by webpack.

Dependencies as Parameter

require.ensure(['./b.js'], function(require) {
    require('./c.js');
});

In the above code, b.js and c.js are bundled together and split from the main bundle. But only the contents of c.js are executed. The contents of b.js are only made available and not executed. To execute b.js, we will have to require it in a sync manner like require('./b.js') for the JavaScript to get executed.

Examples

Weblinks


Contributors