Skip to content

Files

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Latest commit

caff618 · May 27, 2022

History

History
472 lines (377 loc) · 18.1 KB

code-splitting.mdx

File metadata and controls

472 lines (377 loc) · 18.1 KB
title sort contributors translators related
代码分离
5
pksjce
pastelsky
simon04
jonwheeler
johnstew
shinxi
tomtasche
levy9527
rahulcs
chrisVillanueva
rafde
bartushek
shaunwallace
skipjack
jakearchibald
TheDutchCoder
rouzbeh84
shaodahong
sudarsangp
kcolton
efreitasn
EugeneHlushko
Tiendo1011
byzyk
AnayaDesign
wizardofhogwarts
maximilianschmelzer
smelukov
chenxsan
Adarah
atesgoral
snitin315
artem-malko
QC-L
jacob-lcs
dear-lizhihua
title url
webpack 中的 <link rel="prefetch/preload" />
title url
Chrome 中的预加载、预获取和优先级(Preload, Prefetch And Priorities)
title url
使用用 <link rel="preload" /> 预加载内容

T> 本指南继续沿用 起步 中的示例代码。请确保你已熟悉这些指南中提供的示例以及输出管理章节。

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

入口起点(entry point)
You can't use 'macro parameter character #' in math mode

$#entry-points$

这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。先来看看如何从 main bundle 中分离 another module(另一个模块):

project

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- another-module.js
|- /node_modules

another-module.js

import _ from 'lodash';

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

webpack.config.js

 const path = require('path');

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

这将生成如下构建结果:

...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms

正如前面提到的,这种方式存在一些隐患:

  • 如果入口 chunk 之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle 中。
  • 这种方法不够灵活,并且不能动态地将核心应用程序逻辑中的代码拆分出来。

以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js 中也引入过 lodash,这样就在两个 bundle 中造成重复引用。在下一章节会移除重复的模块。

防止重复(prevent duplication)
You can't use 'macro parameter character #' in math mode

$#prevent-duplication$

入口依赖
You can't use 'macro parameter character #' in math mode

$#entry-dependencies$

配置 dependOn option 选项,这样可以在多个 chunk 之间共享模块:

webpack.config.js

 const path = require('path');

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

如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: 'single',否则还会遇到这里所述的麻烦。

webpack.config.js

 const path = require('path');

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

构建结果如下:

...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms

由上可知,除了生成 shared.bundle.jsindex.bundle.jsanother.bundle.js 之外,还生成了一个 runtime.bundle.js 文件。

尽管可以在 webpack 中允许每个页面使用多入口,应尽可能避免使用多入口的入口:entry: { page: ['./analytics', './app'] }。如此,在使用 async 脚本标签时,会有更好的优化以及一致的执行顺序。

SplitChunksPlugin
You can't use 'macro parameter character #' in math mode

$#splitchunksplugin$

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

webpack.config.js

  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',
+     },
+   },
  };

使用 optimization.splitChunks 配置选项之后,现在应该可以看出,index.bundle.jsanother.bundle.js 中已经移除了重复的依赖模块。需要注意的是,插件将 lodash 分离到单独的 chunk,并且将其从 main bundle 中移除,减轻了大小。执行 npm run build 查看效果:

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms

以下是由社区提供,一些对于代码分离很有帮助的 plugin 和 loader:

动态导入(dynamic import)
You can't use 'macro parameter character #' in math mode

$#dynamic-imports$

当涉及到动态代码拆分时,webpack 提供了两个类似的技术。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。让我们先尝试使用第一种……

W> import() 调用会在内部用到 promises。如果在旧版本浏览器中(例如,IE 11)使用 import(),记得使用一个 polyfill 库(例如 es6-promisepromise-polyfill),来 shim Promise

在我们开始之前,先从上述示例的配置中移除掉多余的 entryoptimization.splitChunks,因为接下来的演示中并不需要它们:

webpack.config.js

 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',
-    },
-  },
 };

我们将更新我们的项目,移除现在未使用的文件:

project

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
- |- another-module.js
|- /node_modules

现在,我们不再使用 statically import(静态导入) lodash,而是通过 dynamic import(动态导入) 来分离出一个 chunk:

src/index.js

-import _ from 'lodash';
-
-function component() {
+function getComponent() {
   const element = document.createElement('div');

-  // Lodash, now imported by this script
-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  return import('lodash')
+    .then(({ default: _ }) => {
+      const element = document.createElement('div');
+
+      element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-  return element;
+      return element;
+    })
+    .catch((error) => 'An error occurred while loading the component');
 }

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

我们之所以需要 default,是因为 webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象,更多有关背后原因的信息,请阅读 webpack 4: import() and CommonJs

让我们执行 webpack,查看 lodash 是否会分离到一个单独的 bundle:

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
  ./src/index.js 434 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms

由于 import() 会返回一个 promise,因此它可以和 async 函数一起使用。下面是如何通过 async 函数简化代码:

src/index.js

-function getComponent() {
+async function getComponent() {
   const element = document.createElement('div');
+  const { default: _ } = await import('lodash');

-  return import('lodash')
-    .then(({ default: _ }) => {
-      const element = document.createElement('div');
+  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-      element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-      return element;
-    })
-    .catch((error) => 'An error occurred while loading the component');
+  return element;
 }

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

T> 在稍后示例中,可能会根据计算后的变量(computed variable)导入特定模块时,可以通过向 import() 传入一个 动态表达式

预获取/预加载模块(prefetch/preload module)
You can't use 'macro parameter character #' in math mode

$#prefetchingpreloading-modules$

Webpack v4.6.0+ 增加了对预获取和预加载的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 "resource hint(资源提示)",来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。

LoginButton.js

//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

T> 只要父 chunk 完成加载,webpack 就会添加 prefetch hint(预取提示)。

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

下面这个简单的 preload 示例中,有一个 Component,依赖于一个较大的 library,所以应该将其分离到一个独立的 chunk 中。

我们假想这里的图表组件 ChartComponent 组件需要依赖一个体积巨大的 ChartingLibrary 库。它会在渲染时显示一个 LoadingIndicator(加载进度条) 组件,然后立即按需导入 ChartingLibrary

ChartComponent.js

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

在页面中使用 ChartComponent 时,在请求 ChartComponent.js 的同时,还会通过 <link rel="preload"> 请求 charting-library-chunk。假定 page-chunk 体积很小,很快就被加载好,页面此时就会显示 LoadingIndicator(加载进度条) ,等到 charting-library-chunk 请求完成,LoadingIndicator 组件才消失。启动仅需要很少的加载时间,因为只进行单次往返,而不是两次往返。尤其是在高延迟环境下。

T> 不正确地使用 webpackPreload 会有损性能,请谨慎使用。

<<<<<<< HEAD 有时你需要自己控制预加载。例如,任何动态导入的预加载都可以通过异步脚本完成。这在流式服务器端渲染的情况下很有用。

Sometimes you need to have your own control over preload. For example, preload of any dynamic import can be done via async script. This can be useful in case of streaming server side rendering.

20e47f2977b4fbb0bc2c3b9c965f07cbb71ee781

const lazyComp = () =>
  import('DynamicComponent').catch((error) => {
    // 在发生错误时做一些处理
    // 例如,我们可以在网络错误的情况下重试请求
  });

如果在 webpack 开始加载该脚本之前脚本加载失败(如果该脚本不在页面上,webpack 只是创建一个 script 标签来加载其代码),则该 catch 处理程序将不会启动,直到 chunkLoadTimeout 未通过。此行为可能是意料之外的。但这是可以解释的 - webpack 不能抛出任何错误,因为 webpack 不知道那个脚本失败了。Webpack 将在错误发生后立即将 onerror 处理脚本添加到 script 中。

为了避免上述问题,你可以添加自己的 onerror 处理脚本,将会在错误发生时移除该 script。

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

在这种情况下,错误的 script 将被删除。Webpack 将创建自己的 script,并且任何错误都将被处理而没有任何超时。

bundle 分析(bundle analysis)
You can't use 'macro parameter character #' in math mode

$#bundle-analysis$

一旦开始分离代码,一件很有帮助的事情是,分析输出结果来检查模块在何处结束。 官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:

  • webpack-chart: webpack stats 可交互饼图。
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。
  • webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
  • bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。

下一步
You can't use 'macro parameter character #' in math mode

$#next-steps$

接下来,查看 延迟加载 来学习如何在实际一个真实应用程序中使用 import() 的具体示例,以及查看 缓存 来学习如何有效地分离代码。