Webpack 系列第四篇:Chunk 分包规则详解

全文 2500 字,阅读时长约 30 分钟。若是以为文章有用,欢迎点赞关注,但写做实属不易,未经做者赞成,禁止任何形式转载!!!

背景

在前面系列文章提到,webpack 实现中,原始的资源模块以 Module 对象形式存在、流转、解析处理。javascript

Chunk 则是输出产物的基本组织单位,在生成阶段 webpack 按规则将 entry 及其它 Module 插入 Chunk 中,以后再由 SplitChunksPlugin 插件根据优化规则与 ChunkGraphChunk 作一系列的变化、拆解、合并操做,从新组织成一批性能(可能)更高的 Chunks 。运行完毕以后 webpack 继续将 chunk 一一写入物理文件中,完成编译工做。java

综上,Module 主要做用在 webpack 编译过程的前半段,解决原始资源“如何读”的问题;而 Chunk 对象则主要做用在编译的后半段,解决编译产物“如何写”的问题,二者合做搭建起 webpack 搭建主流程。webpack

Chunk 的编排规则很是复杂,涉及 entry、optimization 等诸多配置项,我打算分红两篇文章分别讲解基本分包规则、SplitChunksPlugin 分包优化规则,本文将集中在第一部分,讲解 entry、异步模块、runtime 三条规则的细节与原理。web

image.png

关注公众号【Tecvan】,回复【1】,获取 Webpack 知识体系脑图

默认分包规则

Webpack 4 以后编译过程大体上能够拆解为四个阶段(参考:[万字总结] 一文吃透 Webpack 核心原理):promise

在构建(make) 阶段,webpack 从 entry 出发根据模块间的引用关系(require/import) 逐步构建出模块依赖关系图(ModuleDependencyGraph),依赖关系图表达了模块与模块之间互相引用的前后次序,基于这种次序 webpack 就能够推断出模块运行以前须要先执行那些依赖模块,也就能够进一步推断出那些模块应该打包在一块儿,那些模块能够延后加载(异步执行),关于模块依赖图的更多信息,能够参考我另外一篇文章 《有点难的 webpack 知识点:Dependency Graph 深度解析》。架构

到了生成(seal) 阶段,webpack 会根据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规则有:异步

  • 同一个 entry 下触达到的模块组织成一个 chunk
  • 异步模块单独组织为一个 chunk
  • entry.runtime 单独组织成一个 chunk

默认规则集中在 compilation.seal 函数实现,seal 核心逻辑运行结束后会生成一系列的 ChunkChunkGroupChunkGraph 对象,后续如 SplitChunksPlugin 插件会在 Chunk 系列对象上作进一步的拆解、优化,最终反映到输出上才会表现出复杂的分包结果。async

咱们聊聊默认生成规则。模块化

Entry 分包处理

重点:seal 阶段遍历 entry 对象,为每个 entry 单独生成 chunk,以后再根据模块依赖图将 entry 触达到的全部模块打包进 chunk 中。

在生成阶段,Webpack 首先根据遍历用户提供的 entry 属性值,为每个 entry 建立 Chunk 对象,好比对于以下配置:函数

module.exports = {
  entry: {
    main: "./src/main",
    home: "./src/home",
  }
};

Webpack 遍历 entry 对象属性并建立出 chunk[main]chunk[home] 两个对象,此时两个 chunk 分别包含 mainhome 模块:

初始化完毕后,Webpack 会读取 ModuleDependencyGraph 的内容,将 entry 所对应的内容塞入对应的 chunk (发生在 webpack/lib/buildChunkGrap.js 文件)。好比对于以下文件依赖:

main.js 以同步方式直接或间接引用了 a/b/c/d 四个文件,分析 ModuleDependencyGraph 过程会逐步将 a/b/c/d 模块逐步添加到 chunk[main] 中,最终造成:

PS: 基于动态加载生成的 chunk 在 webpack 官方文档中,一般称之为 Initial chunk

异步模块分包处理

重点:分析 ModuleDependencyGraph 时,每次遇到异步模块都会为之建立单独的 Chunk 对象,单独打包异步模块。

Webpack 4 以后,只须要用异步语句 require.ensure("./xx.js")import("./xx.js") 方式引入模块,就能够实现模块的动态加载,这种能力本质也是基于 Chunk 实现的。

Webpack 生成阶段中,遇到异步引入语句时会为该模块单独生成一个 chunk 对象,并将其子模块都加入这个 chunk 中。例如对于下面的例子:

// index.js, entry 文件
import 'sync-a'
import 'sync-b'

import('async-c')

index.js 中,以同步方式引入 sync-async-b;以异步方式引入 async-a 模块;同时,在 · 中以同步方式引入 · 模块。对应的模块依赖如:

此时,webpack 会为入口 index.js、异步模块 async-a.js 分别建立分包,造成以下数据:

这里须要引入一个新的概念 —— Chunk 间的父子关系。由 entry 生成的 Chunk 之间相互孤立,没有必然的先后依赖关系,但异步生成的 Chunk 则不一样,引用者(上例 index.js 块)须要在特定场景下使用被引用者(上例 async-a 块),二者间存在单向依赖关系,在 webpack 中称引用者为 parent、被引用者为 child,分别存放在 ChunkGroup._parentsChunkGroup._children 属性中。

上述分包方案默认状况下会生成两个文件:

  • 入口 index 对应的 index.js
  • 异步模块 async-a 对应的 src_async-a_js.js

运行时,webpack 在 index.js 中使用 promise 及 __webpack_require__.e 方法异步载入并运行文件 src_async-a_js.js ,从而实现动态加载。

PS: 基于异步模块的 chunk 在 webpack 官方文档中,一般称之为 Async chunk

Runtime 分包

重点: Webpack 5 以后还能根据 entry.runtime 配置单独打包运行时代码。

除了 entry、异步模块外,webpack 5以后还支持基于 runtime 的分包规则。除业务代码外,Webpack 编译产物中还须要包含一些用于支持 webpack 模块化、异步加载等特性的支撑性代码,这类代码在 webpack 中被统称为 runtime。举个例子,产物中一般会包含以下代码:

/******/ (() => {
  // webpackBootstrap
  /******/ var __webpack_modules__ = {}; // The module cache
  /************************************************************************/
  /******/ /******/ var __webpack_module_cache__ = {}; // The require function
  /******/

  /******/ /******/ function __webpack_require__(moduleId) {

    /******/ /******/ __webpack_modules__[moduleId](
      module,
      module.exports,
      __webpack_require__
    ); // Return the exports of the module
    /******/

    /******/ /******/ return module.exports;
    /******/
  } // expose the modules object (__webpack_modules__)
  /******/

  /******/ /******/ __webpack_require__.m = __webpack_modules__; /* webpack/runtime/compat get default export */
  /******/

  // ...
})();

编译时,Webpack 会根据业务代码决定输出那些支撑特性的运行时代码(基于 Dependency 子类),例如:

  • 须要 __webpack_require__.f__webpack_require__.r 等功能实现最起码的模块化支持
  • 若是用到动态加载特性,则须要写入 __webpack_require__.e 函数
  • 若是用到 Module Federation 特性,则须要写入 __webpack_require__.o 函数
  • 等等

虽然每段运行时代码可能都很小,但随着特性的增长,最终结果会愈来愈大,特别对于多 entry 应用,在每一个入口都重复打包一份类似的运行时代码显得有点浪费,为此 webpack 5 专门提供了 entry.runtime 配置项用于声明如何打包运行时代码。用法上只需在 entry 项中增长字符串形式的 runtime 值,例如:

module.exports = {
  entry: {
    index: { import: "./src/index", runtime: "solid-runtime" },
  }
};

Webpack 执行完 entry、异步模块分包后,开始遍历 entry 配置判断是否带有 runtime 属性,若是有则建立以 runtime 值为名的 Chunk,所以,上例配置将生成两个chunk:chunk[index.js]chunk[solid-runtime],并据此最终产出两个文件:

  • 入口 index 对应的 index.js 文件
  • 运行时配置对应的 solid-runtime.js 文件

在多 entry 场景中,只要为每一个 entry 都设定相同的 runtime 值,webpack 运行时代码最终就会集中写入到同一个 chunk,例如对于以下配置:

module.exports = {
  entry: {
    index: { import: "./src/index", runtime: "solid-runtime" },
    home: { import: "./src/home", runtime: "solid-runtime" },
  }
};

入口 index、home 共享相同的 runtime ,最终生成三个 chunk,分别为:

同时生成三个文件:

  • 入口 index 对应的 index.js
  • 入口 index 对应的 home.js
  • 运行时代码对应的 solid-runtime.js

分包规则的问题

至此,webpack 分包规则的基本逻辑就介绍完毕了,实现上,大部分功能代码都集中在:

  • webpack/lib/compilation.js 文件的 seal 函数
  • webpack/lib/buildChunkGraph.jsbuildChunkGraph 函数

默认分包规则最大的问题是没法解决模块重复,若是多个 chunk 同时包含同一个 module,那么这个 module 会被不受限制地重复打包进这些 chunk。好比假设咱们有两个入口 main/index 同时依赖了同一个模块:

默认状况下,webpack 不会对此作额外处理,只是单纯地将 c 模块同时打包进 main/index 两个 chunk,最终造成:

能够看到 chunk 间互相孤立,模块 c 被重复打包,对最终产物可能形成没必要要的性能损耗!

为了解决这个问题,webpack 3 引入 CommonChunkPlugin 插件试图将 entry 之间的公共依赖提取成单独的 chunk,但 CommonChunkPlugin 本质上是基于 Chunk 之间简单的父子关系链实现的,很难推断出提取出的第三个包应该做为 entry 的父 chunk 仍是子 chunk,CommonChunkPlugin 统一处理为父 chunk,某些状况下反而对性能形成了不小的负面影响。

在 webpack 4 以后则引入了更负责的设计 —— ChunkGroup 专门实现关系链管理,配合 SplitChunksPlugin 可以更高效、智能地实现启发式分包,这里的内容很复杂,我打算拆开来在下一篇文章再讲,感兴趣的同窗记得关注。

下节预告

后面我还会继续 focus 在 chunk 相关功能与核心实现原理,内容包括:

  • webpack 4 以后引入 ChunkGroup 的引入解决了什么问题,为何能极大优化分包功能
  • webpack 5 引入的 ChunkGraph 解决了什么问题
  • Chunk、ChunkGroup、ChunkGraph 分别实现什么能力,互相之间如何协做,为何要作这样的拆分
  • SplitChunksPlugin 插件作了那些分包优化,以及咱们能够从中学到什么插件开发技巧
  • 站在应用、性能的角度,有那些分包最佳实践

感兴趣的同窗必定要记得点赞关注,您的反馈将是我持续创做的巨大动力!

image.png

往期文章: