全文 2500 字,浏览时长约 30 分钟。如果感觉文章有用,欢送点赞关注,但写作实属不易,未经作者批准,禁止任何模式转载!!!
背景
在后面系列文章提到,webpack 实现中,原始的资源模块以 Module
对象模式存在、流转、解析解决。
而 Chunk
则是输入产物的根本组织单位,在生成阶段 webpack 按规定将 entry
及其它 Module
插入 Chunk
中,之后再由 SplitChunksPlugin
插件依据优化规定与 ChunkGraph
对 Chunk
做一系列的变动、拆解、合并操作,从新组织成一批性能 (可能) 更高的 Chunks
。运行结束之后 webpack 持续将 chunk
一一写入物理文件中,实现编译工作。
综上,Module
次要作用在 webpack 编译过程的前半段,解决原始资源“如何读 ”的问题;而 Chunk 对象则次要作用在编译的后半段,解决编译产物“ 如何写”的问题,两者单干搭建起 webpack 搭建主流程。
Chunk 的编排规定非常复杂,波及 entry、optimization 等诸多配置项,我打算分成两篇文章别离解说根本分包规定、SplitChunksPlugin
分包优化规定,本文将集中在第一局部,解说 entry、异步模块、runtime 三条规定的细节与原理。
关注公众号【Tecvan】,回复【1】,获取 Webpack 常识体系脑图
默认分包规定
Webpack 4 之后编译过程大抵上能够拆解为四个阶段(参考:[万字总结] 一文吃透 Webpack 外围原理):
在构建(make) 阶段,webpack 从 entry 登程依据模块间的援用关系(require/import) 逐渐构建出模块依赖关系图(ModuleDependencyGraph),依赖关系图表白了模块与模块之间相互援用的先后秩序,基于这种秩序 webpack 就能够推断出模块运行之前须要先执行那些依赖模块,也就能够进一步推断出那些模块应该打包在一起,那些模块能够延后加载(异步执行),对于模块依赖图的更多信息,能够参考我另一篇文章《有点难的 webpack 知识点:Dependency Graph 深度解析》。
到了生成(seal) 阶段,webpack 会依据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规定有:
- 同一个
entry
下触达到的模块组织成一个 chunk - 异步模块独自组织为一个 chunk
entry.runtime
独自组织成一个 chunk
默认规定集中在 compilation.seal
函数实现,seal 外围逻辑运行完结后会生成一系列的 Chunk
、ChunkGroup
、ChunkGraph
对象,后续如 SplitChunksPlugin
插件会在 Chunk
系列对象上做进一步的拆解、优化,最终反映到输入上才会体现出简单的分包后果。
咱们聊聊默认生成规定。
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 别离蕴含 main
、home
模块:
初始化结束后,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-a
、sync-b
;以异步形式引入 async-a
模块;同时,在 · 中以同步形式引入 · 模块。对应的模块依赖如:
此时,webpack 会为入口 index.js
、异步模块 async-a.js
别离创立分包,造成如下数据:
这里须要引入一个新的概念 —— Chunk
间的父子关系。由 entry
生成的 Chunk
之间互相孤立,没有必然的前后依赖关系,但异步生成的 Chunk
则不同,援用者 (上例 index.js
块) 须要在特定场景下应用被援用者(上例 async-a
块),两者间存在单向依赖关系,在 webpack 中称援用者为 parent、被援用者为 child,别离寄存在 ChunkGroup._parents
、ChunkGroup._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.js
的buildChunkGraph
函数
默认分包规定最大的问题是无奈解决模块反复,如果多个 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 插件做了那些分包优化,以及咱们能够从中学到什么插件开发技巧
- 站在利用、性能的角度,有那些分包最佳实际
感兴趣的同学肯定要记得点赞关注,您的反馈将是我继续创作的微小能源!
往期文章:
- [万字总结] 一文吃透 Webpack 外围原理
- [源码解读] Webpack 插件架构深度解说
- 十分钟精进 Webpack:module.issuer 属性详解
- 分享几个 Webpack 实用剖析工具
- 有点难的 webpack 知识点:Dependency Graph 深度解析