Webpack Code Splitting

In the past few days, when watching the webpack teaching video, I had a little doubt about the code separation, so I went to read the official doc, and now I have sorted out the translation of the original document and my understanding of some of the content.

Code separation is one of the most noticeable features in webpack. This feature enables code to be separated into different bundles, which can then be loaded on demand or in parallel. Code separation can be used to obtain smaller bundles and control resource loading priority. If used properly, it can greatly affect loading time.

There are three commonly used code separation methods.

  • entry point: use entry Configure to manually separate code.
  • Prevention of duplication: use SplitChunksPlugin Deduplicate and separate chunks.
    Dynamic import: code is separated by internal connection function calls in the module.

First of all, here is to explain the difference and connection between bundle and chunk:

The specific explanation can be seen这里, but in summary it is:

  1. A module is a module. It can be an ESM module or a commonJS or AMD module
  2. The module file operated during the packaging process is called a chunk. For example, loading a module asynchronously is a chunk
  3. bundle is the last packaged file, the final file can be exactly the same as the chunk length, but in most cases it is a collection of multiple chunks

The entry point (entry)

This is by far the simplest and most intuitive way to separate code. However, this way is more manually configured and has some hidden dangers, which we will solve. Let’s first take a look at how to separate another module from the main bundle:

project

1
2
3
4
5
6
7
8
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
+ |- another-module.js
|- /node_modules

another-module.js

1
2
3
4
5
import _ from 'lodash';

console.log(
_.join(['Another', 'module', 'loaded!'], ' ')
);

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require('path');

module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
+ another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

About the output.filename in the above configuration file [name],它的作用是可以让打包生成的文件名称是入口[文件名称.bundle.js],这里还可以配置许多其他的配置,详细的配置可以看官方文档

This will generate the following build result:

1
2
3
4
5
6
7
...
Asset Size Chunks Chunk Names
another.bundle.js 550 KiB another [emitted] another
index.bundle.js 550 KiB index [emitted] index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
...

As mentioned earlier, there are some pitfalls in this approach:

  • If there are duplicate modules between the entry chunks, those duplicate modules will be introduced into each bundle. Because here all files in the dependencies generated by each entry file will be packaged into their respective same file.
    This method is not flexible enough and cannot dynamically split the code in the core application logic.

The first of these two points is undoubtedly a serious problem for our example, because we also introduced lodash in ‘./src/index.js’, which results in repeated references in both bundles. We can use SplitChunksPlugin Plugin to remove duplicate modules.

Prevent duplication

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的 entry chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件,将前面示例中重复的 lodash 模块去除:

CommonsChunkPlugin

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  const path = require('path');

module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all'
+ }
+ }
};

Use optimization.splitChunks Configuration options, you can now see that duplicate dependencies have been removed from’index.bundle.js’ and’another.bundle.js’. It should be noted that this plugin removes the heavy burden of’lodash ‘from the main bundle and then separates it into a separate chunk. Execute’npm run build’ to see the effect:

1
2
3
4
5
6
7
8
...
Asset Size Chunks Chunk Names
another.bundle.js 5.95 KiB another [emitted] another
index.bundle.js 5.89 KiB index [emitted] index
vendors~another~index.bundle.js 547 KiB vendors~another~index [emitted] vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
...

Here are some plugins and loaders provided by the community that are helpful for code separation:

Dynamic import

When it comes to dynamic code splitting, webpack provides two similar techniques. The first, and recommended, way is to use ECMAScript 提案 Of import() 语法 To achieve dynamic import. The second is the legacy function of webpack, using webpack-specific require.ensureLet’s try using the first one first

import()

Before we start, let’s remove the redundant from the configuration entry And optimization.splitChunks, because they are not needed in the following demonstration:

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  const path = require('path');

module.exports = {
mode: 'development',
entry: {
+ index: './src/index.js'
- index: './src/index.js',
- another: './src/another-module.js'
},
output: {
filename: '[name].bundle.js',
+ chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
- optimization: {
- splitChunks: {
- chunks: 'all'
- }
- }
};

Note that’chunkFilename ‘is used here, which determines the name of the non-entry chunk. For more information on’chunkFilename’, see 输出 Doc. Update our project to remove files that will not be used now:

project

1
2
3
4
5
6
7
8
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- /src
|- index.js
- |- another-module.js
|- /node_modules

Now, instead of statically importing lodash, we use dynamic import to separate a chunk:

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
- var element = document.createElement('div');
-
- // Lodash, now imported by this script
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+ return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
+ var element = document.createElement('div');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+ return element;
+
+ }).catch(error => 'An error occurred while loading the component');
}

- document.body.appendChild(component());
+ getComponent().then(component => {
+ document.body.appendChild(component);
+ })

The reason we need to use’default ‘here is that starting from webpack v4, when importing CommonJS modules, instead of parsing the imported module to the value of’module.exports’, an artificial namespace object is created for the CommonJS module. For more information on the reason behind this, please read webpack 4: import() 和 CommonJs

Note that in the comment we provided’webpackChunkName ‘. This will name the split bundle lodash.bundle.js instead of’[id].bundle.js。想了解更多关于 webpackChunkName 和其他可用选项,请查看 [import()`](https://webpack.docschina.org/api/module-methods/#import-) Doc. Let’s execute webpack and see that lodash detached a separate bundle:

1
2
3
4
5
6
...
Asset Size Chunks Chunk Names
index.bundle.js 7.88 KiB index [emitted] index
vendors~lodash.bundle.js 547 KiB vendors~lodash [emitted] vendors~lodash
Entrypoint index = index.bundle.js
...

Since import () returns a promise, it can be combined with async 函数Used together. However, a preprocessor like Babel and Syntax Dynamic Import Babel PluginHere’s how to simplify the code with an async function:

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- function getComponent() {
+ async function getComponent() {
- return import(/* webpackChunkName: "lodash" */ 'lodash').then({ default: _ } => {
- var element = document.createElement('div');
-
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- return element;
-
- }).catch(error => 'An error occurred while loading the component');
+ var element = document.createElement('div');
+ const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
+
+ element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+
+ return element;
}

getComponent().then(component => {
document.body.appendChild(component);
});

Prefetch/preload modules (prefetch/preload

Webpack v4.6.0 + added support for prefetching and preloading.

When declaring import, you can use the following built-in directives to make webpack output “resource hint” to inform the browser:

  • prefetch: resources that may be needed under certain navigation in the future
  • preload (preload): resources may be required under the current navigation

In the following simple example of prefetch, there is a HomePage component that internally renders a LoginButton component, and then loads the LoginModal component on demand after clicking.

LoginButton.js

1
2
//...
import(/* webpackPrefetch: true */ 'LoginModal');

This generates and appends to the page header, instructing the browser to prefetch the login-modal-chunk.js file at idle time.

As long as the father

Compared with the prefetch instruction, the preload instruction has many differences.

  • The preload chunk will start loading in parallel when the parent chunk is loaded. The prefetch chunk will start loading after the parent chunk is loaded.
  • preload chunk has medium priority and downloads immediately. Prefetch chunk downloads when the browser is idle.
  • The preload chunk will be requested immediately in the parent chunk for the present moment. The prefetch chunk will be used at some future moment.
    Different levels of browser support.

In the simple preload example below, there is a Component that depends on a larger library, so it should be separated into a separate chunk.

Let’s imagine that the ChartComponent component here relies on the huge ChartingLibrary library. It will display a LoadingIndicator (Load Progress Bar) component during rendering, and then immediately import the ChartingLibrary on demand:

ChartComponent.js

1
2
//...
import(/* webpackPreload: true */ 'ChartingLibrary');

When using’ChartComponent 'in a page, at the same time as requesting ChartComponent.js, charting-library-chunk will also be requested through ". Assuming that the page-chunk is small and loads quickly, the page will display’LoadingIndicator (Load Progress Bar) ’ at this time, and wait until the’charting-library-chunk 'request completes, and the LoadingIndicator component disappears. Startup requires very little loading time because only one round trip is made instead of two. Especially in high latency environments.

Incorrect use

For other packaging methods supported by webpack, you can see这篇文档