有點(diǎn)難的知識(shí)點(diǎn): Webpack Chunk 分包規(guī)則詳解
背景
在前面系列文章提到,webpack 實(shí)現(xiàn)中,原始的資源模塊以 Module 對(duì)象形式存在、流轉(zhuǎn)、解析處理。
而 Chunk 則是輸出產(chǎn)物的基本組織單位,在生成階段 webpack 按規(guī)則將 entry 及其它 Module 插入 Chunk 中,之后再由 SplitChunksPlugin 插件根據(jù)優(yōu)化規(guī)則與 ChunkGraph 對(duì) Chunk 做一系列的變化、拆解、合并操作,重新組織成一批性能(可能)更高的 Chunks 。運(yùn)行完畢之后 webpack 繼續(xù)將 chunk 一一寫入物理文件中,完成編譯工作。
綜上,Module 主要作用在 webpack 編譯過程的前半段,解決原始資源“「如何讀」”的問題;而 Chunk 對(duì)象則主要作用在編譯的后半段,解決編譯產(chǎn)物“「如何寫」”的問題,兩者合作搭建起 webpack 搭建主流程。
Chunk 的編排規(guī)則非常復(fù)雜,涉及 entry、optimization 等諸多配置項(xiàng),我打算分成兩篇文章分別講解基本分包規(guī)則、SplitChunksPlugin 分包優(yōu)化規(guī)則,本文將集中在第一部分,講解 entry、異步模塊、runtime 三條規(guī)則的細(xì)節(jié)與原理。
默認(rèn)分包規(guī)則
Webpack 4 之后編譯過程大致上可以拆解為四個(gè)階段(參考:[萬字總結(jié)] 一文吃透 Webpack 核心原理):
在構(gòu)建(make) 階段,webpack 從 entry 出發(fā)根據(jù)模塊間的引用關(guān)系(require/import) 逐步構(gòu)建出模塊依賴關(guān)系圖(ModuleDependencyGraph),依賴關(guān)系圖表達(dá)了模塊與模塊之間互相引用的先后次序,基于這種次序 webpack 就可以推斷出模塊運(yùn)行之前需要先執(zhí)行那些依賴模塊,也就可以進(jìn)一步推斷出那些模塊應(yīng)該打包在一起,那些模塊可以延后加載(異步執(zhí)行),關(guān)于模塊依賴圖的更多信息,可以參考我另一篇文章 《有點(diǎn)難的 webpack 知識(shí)點(diǎn):Dependency Graph 深度解析》。
到了生成(seal) 階段,webpack 會(huì)根據(jù)模塊依賴圖的內(nèi)容組織分包 —— Chunk 對(duì)象,默認(rèn)的分包規(guī)則有:
- 同一個(gè) entry 下觸達(dá)到的模塊組織成一個(gè) chunk
- 異步模塊單獨(dú)組織為一個(gè) chunk
- entry.runtime 單獨(dú)組織成一個(gè) chunk
默認(rèn)規(guī)則集中在 compilation.seal 函數(shù)實(shí)現(xiàn),seal 核心邏輯運(yùn)行結(jié)束后會(huì)生成一系列的 Chunk、ChunkGroup、ChunkGraph 對(duì)象,后續(xù)如 SplitChunksPlugin 插件會(huì)在 Chunk 系列對(duì)象上做進(jìn)一步的拆解、優(yōu)化,最終反映到輸出上才會(huì)表現(xiàn)出復(fù)雜的分包結(jié)果。
我們聊聊默認(rèn)生成規(guī)則。
Entry 分包處理
重點(diǎn):seal 階段遍歷 entry 對(duì)象,為每一個(gè) entry 單獨(dú)生成 chunk,之后再根據(jù)模塊依賴圖將 entry 觸達(dá)到的所有模塊打包進(jìn) chunk 中。
在生成階段,Webpack 首先根據(jù)遍歷用戶提供的 entry 屬性值,為每一個(gè) entry 創(chuàng)建 Chunk 對(duì)象,比如對(duì)于如下配置:
- module.exports = {
- entry: {
- main: "./src/main",
- home: "./src/home",
- }
- };
Webpack 遍歷 entry 對(duì)象屬性并創(chuàng)建出 chunk[main] 、chunk[home] 兩個(gè)對(duì)象,此時(shí)兩個(gè) chunk 分別包含 main 、home 模塊:
初始化完畢后,Webpack 會(huì)讀取 ModuleDependencyGraph 的內(nèi)容,將 entry 所對(duì)應(yīng)的內(nèi)容塞入對(duì)應(yīng)的 chunk (發(fā)生在 webpack/lib/buildChunkGrap.js 文件)。比如對(duì)于如下文件依賴:
main.js 以同步方式直接或間接引用了 a/b/c/d 四個(gè)文件,分析 ModuleDependencyGraph 過程會(huì)逐步將 a/b/c/d 模塊逐步添加到 chunk[main] 中,最終形成:
PS: 基于動(dòng)態(tài)加載生成的 chunk 在 webpack 官方文檔中,通常稱之為 「Initial chunk」 。
異步模塊分包處理
重點(diǎn):分析 ModuleDependencyGraph 時(shí),每次遇到異步模塊都會(huì)為之創(chuàng)建單獨(dú)的 Chunk 對(duì)象,單獨(dú)打包異步模塊。
Webpack 4 之后,只需要用異步語句 require.ensure("./xx.js") 或 import("./xx.js") 方式引入模塊,就可以實(shí)現(xiàn)模塊的動(dòng)態(tài)加載,這種能力本質(zhì)也是基于 Chunk 實(shí)現(xiàn)的。
Webpack 生成階段中,遇到異步引入語句時(shí)會(huì)為該模塊單獨(dú)生成一個(gè) chunk 對(duì)象,并將其子模塊都加入這個(gè) chunk 中。例如對(duì)于下面的例子:
- // index.js, entry 文件
- import 'sync-a'
- import 'sync-b'
- import('async-c')
在 index.js 中,以同步方式引入 sync-a、sync-b;以異步方式引入 async-a 模塊;同時(shí),在 async-a 中以同步方式引入 sync-c 模塊。對(duì)應(yīng)的模塊依賴如:
此時(shí),webpack 會(huì)為入口 index.js、異步模塊 async-a.js 分別創(chuàng)建分包,形成如下數(shù)據(jù):
這里需要引入一個(gè)新的概念 —— Chunk 間的父子關(guān)系。由 entry 生成的 Chunk 之間相互孤立,沒有必然的前后依賴關(guān)系,但異步生成的 Chunk 則不同,引用者(上例 index.js 塊)需要在特定場(chǎng)景下使用被引用者(上例 async-a 塊),兩者間存在單向依賴關(guān)系,在 webpack 中稱引用者為 parent、被引用者為 child,分別存放在 ChunkGroup._parents 、ChunkGroup._children 屬性中。
上述分包方案默認(rèn)情況下會(huì)生成兩個(gè)文件:
- 入口 index 對(duì)應(yīng)的 index.js
- 異步模塊 async-a 對(duì)應(yīng)的 src_async-a_js.js
運(yùn)行時(shí),webpack 在 index.js 中使用 promise 及 __webpack_require__.e 方法異步載入并運(yùn)行文件 src_async-a_js.js ,從而實(shí)現(xiàn)動(dòng)態(tài)加載。
PS: 基于異步模塊的 chunk 在 webpack 官方文檔中,通常稱之為 「Async chunk」。
Runtime 分包
重點(diǎn):Webpack 5 之后還能根據(jù) entry.runtime 配置單獨(dú)打包運(yùn)行時(shí)代碼。
除了 entry、異步模塊外,webpack 5之后還支持基于 runtime 的分包規(guī)則。除業(yè)務(wù)代碼外,Webpack 編譯產(chǎn)物中還需要包含一些用于支持 webpack 模塊化、異步加載等特性的支撐性代碼,這類代碼在 webpack 中被統(tǒng)稱為 runtime。舉個(gè)例子,產(chǎn)物中通常會(huì)包含如下代碼:
- /******/ (() => {
- // 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 */
- /******/
- // ...
- })();
編譯時(shí),Webpack 會(huì)根據(jù)業(yè)務(wù)代碼決定輸出那些支撐特性的運(yùn)行時(shí)代碼(基于 Dependency 子類),例如:
- 需要 __webpack_require__.f、__webpack_require__.r 等功能實(shí)現(xiàn)最起碼的模塊化支持
- 如果用到動(dòng)態(tài)加載特性,則需要寫入 __webpack_require__.e 函數(shù)
- 如果用到 Module Federation 特性,則需要寫入 __webpack_require__.o 函數(shù)
- 等等
雖然每段運(yùn)行時(shí)代碼可能都很小,但隨著特性的增加,最終結(jié)果會(huì)越來越大,特別對(duì)于多 entry 應(yīng)用,在每個(gè)入口都重復(fù)打包一份相似的運(yùn)行時(shí)代碼顯得有點(diǎn)浪費(fèi),為此 webpack 5 專門提供了 entry.runtime 配置項(xiàng)用于聲明如何打包運(yùn)行時(shí)代碼。用法上只需在 entry 項(xiàng)中增加字符串形式的 runtime 值,例如:
- module.exports = {
- entry: {
- index: { import: "./src/index", runtime: "solid-runtime" },
- }
- };
Webpack 執(zhí)行完 entry、異步模塊分包后,開始遍歷 entry 配置判斷是否帶有 runtime 屬性,如果有則創(chuàng)建以 runtime 值為名的 Chunk,因此,上例配置將生成兩個(gè)chunk:chunk[index.js] 、chunk[solid-runtime],并據(jù)此最終產(chǎn)出兩個(gè)文件:
- 入口 index 對(duì)應(yīng)的 index.js 文件
- 運(yùn)行時(shí)配置對(duì)應(yīng)的 solid-runtime.js 文件
在多 entry 場(chǎng)景中,只要為每個(gè) entry 都設(shè)定相同的 runtime 值,webpack 運(yùn)行時(shí)代碼最終就會(huì)集中寫入到同一個(gè) chunk,例如對(duì)于如下配置:
- module.exports = {
- entry: {
- index: { import: "./src/index", runtime: "solid-runtime" },
- home: { import: "./src/home", runtime: "solid-runtime" },
- }
- };
入口 index、home 共享相同的 runtime ,最終生成三個(gè) chunk,分別為:
同時(shí)生成三個(gè)文件:
- 入口 index 對(duì)應(yīng)的 index.js
- 入口 index 對(duì)應(yīng)的 home.js
- 運(yùn)行時(shí)代碼對(duì)應(yīng)的 solid-runtime.js
分包規(guī)則的問題
至此,webpack 分包規(guī)則的基本邏輯就介紹完畢了,實(shí)現(xiàn)上,大部分功能代碼都集中在:
- webpack/lib/compilation.js 文件的 seal 函數(shù)
- webpack/lib/buildChunkGraph.js 的 buildChunkGraph 函數(shù)
默認(rèn)分包規(guī)則最大的問題是無法解決模塊重復(fù),如果多個(gè) chunk 同時(shí)包含同一個(gè) module,那么這個(gè) module 會(huì)被不受限制地重復(fù)打包進(jìn)這些 chunk。比如假設(shè)我們有兩個(gè)入口 main/index 同時(shí)依賴了同一個(gè)模塊:
默認(rèn)情況下,webpack 不會(huì)對(duì)此做額外處理,只是單純地將 c 模塊同時(shí)打包進(jìn) main/index 兩個(gè) chunk,最終形成:
可以看到 chunk 間互相孤立,模塊 c 被重復(fù)打包,對(duì)最終產(chǎn)物可能造成不必要的性能損耗!
為了解決這個(gè)問題,webpack 3 引入 CommonChunkPlugin 插件試圖將 entry 之間的公共依賴提取成單獨(dú)的 chunk,但 CommonChunkPlugin 本質(zhì)上是基于 Chunk 之間簡(jiǎn)單的父子關(guān)系鏈實(shí)現(xiàn)的,很難推斷出提取出的第三個(gè)包應(yīng)該作為 entry 的父 chunk 還是子 chunk,CommonChunkPlugin 統(tǒng)一處理為父 chunk,某些情況下反而對(duì)性能造成了不小的負(fù)面影響。
在 webpack 4 之后則引入了更負(fù)責(zé)的設(shè)計(jì) —— ChunkGroup 專門實(shí)現(xiàn)關(guān)系鏈管理,配合 SplitChunksPlugin 能夠更高效、智能地實(shí)現(xiàn)「啟發(fā)式分包」,這里的內(nèi)容很復(fù)雜,我打算拆開來在下一篇文章再講。


































