随着功能的不断迭代,Web 应用变得越来越大,这也导致网页的加载时间越来越长,用户体验越来越差。 Webpack 的代码分割能力(Code Spliting)可以实现应用代码的按需加载,减少应用首次加载的代码量,提升加载速度。

通过代码分割,我们可以在用户进入到一个新页面的时候加载代码,也可以在用户点击某一个按钮的时候加载代码,甚至可以猜测用户下一步要做的事情而提前加载代码。

代码分割的核心目的就是通过在应用中设置多个分割点(split point)来实现按需加载。在分割的文件中还可以再创建分割点,不断分割下去。 整个应用就是构建在这些分割模块之上。

code-split

如何实现代码分割

Webpack 提供了两种实现代码分割的方式:动态 importrequire.ensure。其中 require.ensure 已经不在提倡使用。

动态 import 通过 Promise 来实现:

import(/* webpackChunkName: "optional-name" */ "./module").then(
  module => {...}
).catch(
  error => {...}
);
1
2
3
4
5

在上面的例子中,我们看到一行 webpackChunkName 注释,这是 Webpack 提供的额外的配置选项,webpackChunkName 重新命名了分割出来的模块名称。相同 webpackChunkName 的分割模块将会被合并到一个文件中。除了 webpackChunkName 以外,Webpack 还提供了 webpackModewebpackPrefetchwebpackPrefetch 等选项来自定义模块什么时候被加载以及浏览器如何来加载模块。在服务端渲染(server-side rendering, ssr)中,我们还可以设置 weak 参数。这样避免加载额外的模块。

prefetch 告诉浏览器该资源在不久的将来会被使用,而 preload 告诉浏览器该资源在当前页面下需要使用。通过这些信息,浏览器可以知道需要加载哪些资源。

webpackChunkName 同时还支持 [index][request] 这两个占位符来自定义分割出来的模块名称。

我们可以并行加载多个模块,比如:

Promise.all([import('lunr'), import('../search_index.json')]).then(
  ([lunr, search]) => {
    return {
      index: lunr.Index.load(search.index),
      lines: search.lines,
    };
  }
);
1
2
3
4
5
6
7
8

下面我们在我们的项目中使用代码分割。

src/lazy.js

export default 'Hello from lazy';
1

我们在点击按钮的时候加载这个模块,来修改按钮的文案。

src/component.js

export default (text = 'Hello world') => {
  const element = document.createElement('div');

  element.className = 'rounded bg-red-100 border max-w-md m-4 p-4';
  element.innerHTML = text;
  element.onclick = () =>
    import('./lazy')
      .then((lazy) => {
        element.textContent = lazy.default;
      })
      .catch((err) => console.error(err));

  return element;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

此时,执行 npm start,然后点击按钮,我们可以看到按钮的文案发生了变化。

再执行 npm run build, 我们观察下输出结果:

⬡ webpack: Build Finished
⬡ webpack: assets by chunk 10.8 KiB (name: main)
    asset main.css 7.94 KiB [emitted] (name: main)
    asset main.js 2.91 KiB [emitted] [minimized] (name: main) 1 related asset
  asset index.html 229 bytes [compared for emit]
  asset 958.js 183 bytes [emitted] [minimized] 1 related asset
  Entrypoint main 10.8 KiB (5.61 KiB) = main.css 7.94 KiB main.js 2.91 KiB 1 auxiliary asset
  runtime modules 6.71 KiB 10 modules
  orphan modules 465 bytes [orphan] 2 modules
  code generated modules 597 bytes (javascript) 4.18 MiB (css/mini-extract) [code generated]
    ./src/index.js + 2 modules 564 bytes [built] [code generated]
    css ./node_modules/css-loader/dist/cjs.js!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[0].use[3]!./src/main.css 4.18 MiB [code generated]
    ./src/lazy.js 33 bytes [built] [code generated]
  webpack 5.11.1 compiled successfully in 9113 ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14

958.js 就是分割点,如果我们要自定义文件名称,则可以通过 output.chunkFilename 来设置文件名称。比如设置为 "chunk.[id].js" 可以在输出的文件名称前面加一个 chunk

在运行时控制代码分割

特别是在具有第三方依赖项和高级部署设置的复杂环境中,您可能希望控制从何处加载拆分代码。webpack-require-fromopen in new window 可以解决这个问题,这个包可以重写 import 的路径。

React 中的代码分割

React 的官方文档open in new window中详细说明了代码分割的相关 API。其中最重要的就是 React.lazyReact.Suspense@loadable/componentopen in new window 对这两个 API 做了封装。

禁用代码分割

有时候我们不需要代码分割,比如 SSR,这个时候可以通过如下设置来禁用代码分割:

const config = {
  plugins: [new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 })],
};
1
2
3

总结

代码分割可以加速页面的首次加载性能,提升用户使用体验。

代码分割需要我们自主决定分割点。通常我们通过路由来分割,有时候也会根据某一个特性功能被使用到的时候来创建分割点。

我们可以通过指定相同的名称来将多个分割出来的模块打包到同一个文件中。

在现代框架中,比如 React,提供了一些 API 来支持代码分割,我们可以将这些 API 做一些封装,让使用体验更加友好。

关注微信公众号,获取最新推送~