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

往期文章:

相關文章
相關標籤/搜索