自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Webpack 原理系列九:Tree-Shaking 實(shí)現(xiàn)原理

開發(fā) 前端
Tree Shaking 較早前由 Rich Harris 在 Rollup 中率先實(shí)現(xiàn),Webpack 自 2.0 版本開始接入,至今已經(jīng)成為一種應(yīng)用廣泛的性能優(yōu)化手段。

[[419584]]

一、什么是 Tree ShakingTree-Shaking

是一種基于 ES Module 規(guī)范的 Dead Code Elimination 技術(shù),它會(huì)在運(yùn)行過程中靜態(tài)分析模塊之間的導(dǎo)入導(dǎo)出,確定 ESM 模塊中哪些導(dǎo)出值未曾其它模塊使用,并將其刪除,以此實(shí)現(xiàn)打包產(chǎn)物的優(yōu)化。

圖片

Tree Shaking 較早前由 Rich Harris 在 Rollup 中率先實(shí)現(xiàn),Webpack 自 2.0 版本開始接入,至今已經(jīng)成為一種應(yīng)用廣泛的性能優(yōu)化手段。

1.1 在 Webpack 中啟動(dòng) Tree Shaking

在 Webpack 中,啟動(dòng) Tree Shaking 功能必須同時(shí)滿足三個(gè)條件:

使用 ESM 規(guī)范編寫模塊代碼

配置 optimization.usedExports 為 true,啟動(dòng)標(biāo)記功能

啟動(dòng)代碼優(yōu)化功能,可以通過如下方式實(shí)現(xiàn):

  • 配置 mode = production
  • 配置 optimization.minimize = true
  • 提供 optimization.minimizer 數(shù)組

例如:

  1. // webpack.config.js 
  2. module.exports = { 
  3.   entry: "./src/index"
  4.   mode: "production"
  5.   devtool: false
  6.   optimization: { 
  7.     usedExports: true
  8.   }, 
  9. }; 

1.2 理論基礎(chǔ)

在 CommonJs、AMD、CMD 等舊版本的 JavaScript 模塊化方案中,導(dǎo)入導(dǎo)出行為是高度動(dòng)態(tài),難以預(yù)測(cè)的,例如:

  1. if(process.env.NODE_ENV === 'development'){ 
  2.   require('./bar'); 
  3.   exports.foo = 'foo'

而 ESM 方案則從規(guī)范層面規(guī)避這一行為,它要求所有的導(dǎo)入導(dǎo)出語句只能出現(xiàn)在模塊頂層,且導(dǎo)入導(dǎo)出的模塊名必須為字符串常量,這意味著下述代碼在 ESM 方案下是非法的:

  1. if(process.env.NODE_ENV === 'development'){ 
  2.   import bar from 'bar'
  3.   export const foo = 'foo'

所以,ESM 下模塊之間的依賴關(guān)系是高度確定的,與運(yùn)行狀態(tài)無關(guān),編譯工具只需要對(duì) ESM 模塊做靜態(tài)分析,就可以從代碼字面量中推斷出哪些模塊值未曾被其它模塊使用,這是實(shí)現(xiàn) Tree Shaking 技術(shù)的必要條件。

1.3 示例

對(duì)于下述代碼:

  1. // index.js 
  2. import {bar} from './bar'
  3. console.log(bar); 
  4.  
  5. // bar.js 
  6. export const bar = 'bar'
  7. export const foo = 'foo'

示例中,bar.js 模塊導(dǎo)出了 bar 、foo ,但只有 bar 導(dǎo)出值被其它模塊使用,經(jīng)過 Tree Shaking 處理后,foo 變量會(huì)被視作無用代碼刪除。

二、實(shí)現(xiàn)原理

Webpack 中,Tree-shaking 的實(shí)現(xiàn)一是先「標(biāo)記」出模塊導(dǎo)出值中哪些沒有被用過,二是使用 Terser 刪掉這些沒被用到的導(dǎo)出語句。標(biāo)記過程大致可劃分為三個(gè)步驟:

  • Make 階段,收集模塊導(dǎo)出變量并記錄到模塊依賴關(guān)系圖 ModuleGraph 變量中
  • Seal 階段,遍歷 ModuleGraph 標(biāo)記模塊導(dǎo)出變量有沒有被使用
  • 生成產(chǎn)物時(shí),若變量沒有被其它模塊使用則刪除對(duì)應(yīng)的導(dǎo)出語句

標(biāo)記功能需要配置 optimization.usedExports = true 開啟

也就是說,標(biāo)記的效果就是刪除沒有被其它模塊使用的導(dǎo)出語句,比如:

示例中,bar.js 模塊(左二)導(dǎo)出了兩個(gè)變量:bar 與 foo,其中 foo 沒有被其它模塊用到,所以經(jīng)過標(biāo)記后,構(gòu)建產(chǎn)物(右一)中 foo 變量對(duì)應(yīng)的導(dǎo)出語句就被刪除了。作為對(duì)比,如果沒有啟動(dòng)標(biāo)記功能(optimization.usedExports = false 時(shí)),則變量無論有沒有被用到都會(huì)保留導(dǎo)出語句,如上圖右二的產(chǎn)物代碼所示。

注意,這個(gè)時(shí)候 foo 變量對(duì)應(yīng)的代碼 const foo='foo' 都還保留完整,這是因?yàn)闃?biāo)記功能只會(huì)影響到模塊的導(dǎo)出語句,真正執(zhí)行“「Shaking」”操作的是 Terser 插件。例如在上例中 foo 變量經(jīng)過標(biāo)記后,已經(jīng)變成一段 Dead Code —— 不可能被執(zhí)行到的代碼,這個(gè)時(shí)候只需要用 Terser 提供的 DCE 功能就可以刪除這一段定義語句,以此實(shí)現(xiàn)完整的 Tree Shaking 效果。

接下來我會(huì)展開標(biāo)記過程的源碼,詳細(xì)講解 Webpack 5 中 Tree Shaking 的實(shí)現(xiàn)過程,對(duì)源碼不感興趣的同學(xué)可以直接跳到下一章。

2.1 收集模塊導(dǎo)出

首先,Webpack 需要弄清楚每個(gè)模塊分別有什么導(dǎo)出值,這一過程發(fā)生在 make 階段,大體流程:

關(guān)于 Make 階段的更多說明,請(qǐng)參考前文 [萬字總結(jié)] 一文吃透 Webpack 核心原理 。

1.將模塊的所有 ESM 導(dǎo)出語句轉(zhuǎn)換為 Dependency 對(duì)象,并記錄到 module 對(duì)象的 dependencies 集合,轉(zhuǎn)換規(guī)則:

  • 具名導(dǎo)出轉(zhuǎn)換為 HarmonyExportSpecifierDependency 對(duì)象
  • default 導(dǎo)出轉(zhuǎn)換為 HarmonyExportExpressionDependency 對(duì)象

例如對(duì)于下面的模塊:

  1. export const bar = 'bar'
  2. export const foo = 'foo'
  3.  
  4. export default 'foo-bar' 

對(duì)應(yīng)的dependencies 值為:

2.所有模塊都編譯完畢后,觸發(fā) compilation.hooks.finishModules 鉤子,開始執(zhí)行 FlagDependencyExportsPlugin 插件回調(diào)

3.FlagDependencyExportsPlugin 插件從 entry 開始讀取 ModuleGraph 中存儲(chǔ)的模塊信息,遍歷所有 module 對(duì)象

4.遍歷 module 對(duì)象的 dependencies 數(shù)組,找到所有 HarmonyExportXXXDependency 類型的依賴對(duì)象,將其轉(zhuǎn)換為 ExportInfo 對(duì)象并記錄到 ModuleGraph 體系中

經(jīng)過 FlagDependencyExportsPlugin 插件處理后,所有 ESM 風(fēng)格的 export 語句都會(huì)記錄在 ModuleGraph 體系內(nèi),后續(xù)操作就可以從 ModuleGraph 中直接讀取出模塊的導(dǎo)出值。

參考資料:

[萬字總結(jié)] 一文吃透 Webpack 核心原理

有點(diǎn)難的 webpack 知識(shí)點(diǎn):Dependency Graph 深度解析

2.2 標(biāo)記模塊導(dǎo)出

模塊導(dǎo)出信息收集完畢后,Webpack 需要標(biāo)記出各個(gè)模塊的導(dǎo)出列表中,哪些導(dǎo)出值有被其它模塊用到,哪些沒有,這一過程發(fā)生在 Seal 階段,主流程:

  1. 觸發(fā) compilation.hooks.optimizeDependencies 鉤子,開始執(zhí)行 FlagDependencyUsagePlugin 插件邏輯
  2. 在 FlagDependencyUsagePlugin 插件中,從 entry 開始逐步遍歷 ModuleGraph 存儲(chǔ)的所有 module 對(duì)象
  3. 遍歷 module 對(duì)象對(duì)應(yīng)的 exportInfo 數(shù)組
  4. 為每一個(gè) exportInfo 對(duì)象執(zhí)行 compilation.getDependencyReferencedExports 方法,確定其對(duì)應(yīng)的 dependency 對(duì)象有否被其它模塊使用
  5. 被任意模塊使用到的導(dǎo)出值,調(diào)用 exportInfo.setUsedConditionally 方法將其標(biāo)記為已被使用。
  6. exportInfo.setUsedConditionally 內(nèi)部修改 exportInfo._usedInRuntime 屬性,記錄該導(dǎo)出被如何使用
  7. 結(jié)束

上面是極度簡化過的版本,中間還存在非常多的分支邏輯與復(fù)雜的集合操作,我們抓住重點(diǎn):標(biāo)記模塊導(dǎo)出這一操作集中在 FlagDependencyUsagePlugin 插件中,執(zhí)行結(jié)果最終會(huì)記錄在模塊導(dǎo)出語句對(duì)應(yīng)的 exportInfo._usedInRuntime 字典中。

2.3 生成代碼

經(jīng)過前面的收集與標(biāo)記步驟后,Webpack 已經(jīng)在 ModuleGraph 體系中清楚地記錄了每個(gè)模塊都導(dǎo)出了哪些值,每個(gè)導(dǎo)出值又沒那塊模塊所使用。接下來,Webpack 會(huì)根據(jù)導(dǎo)出值的使用情況生成不同的代碼,例如:

重點(diǎn)關(guān)注 bar.js 文件,同樣是導(dǎo)出值,bar 被 index.js 模塊使用因此對(duì)應(yīng)生成了 __webpack_require__.d 調(diào)用 "bar": ()=>(/* binding */ bar),作為對(duì)比 foo 則僅僅保留了定義語句,沒有在 chunk 中生成對(duì)應(yīng)的 export。

關(guān)于 Webpack 產(chǎn)物的內(nèi)容及 __webpack_require__.d 方法的含義,可參考 Webpack 原理系列六:徹底理解 Webpack 運(yùn)行時(shí) 一文。

這一段生成邏輯均由導(dǎo)出語句對(duì)應(yīng)的 HarmonyExportXXXDependency 類實(shí)現(xiàn),大體的流程:

  1. 打包階段,調(diào)用 HarmonyExportXXXDependency.Template.apply 方法生成代碼
  2. 在 apply 方法內(nèi),讀取 ModuleGraph 中存儲(chǔ)的 exportsInfo 信息,判斷哪些導(dǎo)出值被使用,哪些未被使用
  3. 對(duì)已經(jīng)被使用及未被使用的導(dǎo)出值,分別創(chuàng)建對(duì)應(yīng)的 HarmonyExportInitFragment 對(duì)象,保存到 initFragments 數(shù)組
  4. 遍歷 initFragments 數(shù)組,生成最終結(jié)果

基本上,這一步的邏輯就是用前面收集好的 exportsInfo 對(duì)象未模塊的導(dǎo)出值分別生成導(dǎo)出語句。

2.4 刪除 Dead Code

經(jīng)過前面幾步操作之后,模塊導(dǎo)出列表中未被使用的值都不會(huì)定義在 __webpack_exports__ 對(duì)象中,形成一段不可能被執(zhí)行的 Dead Code 效果,如上例中的 foo 變量:

在此之后,將由 Terser、UglifyJS 等 DCE 工具“搖”掉這部分無效代碼,構(gòu)成完整的 Tree Shaking 操作。

2.5 總結(jié)

綜上所述,Webpack 中 Tree Shaking 的實(shí)現(xiàn)分為如下步驟:

  • 在 FlagDependencyExportsPlugin 插件中根據(jù)模塊的 dependencies 列表收集模塊導(dǎo)出值,并記錄到 ModuleGraph 體系的 exportsInfo 中
  • 在 FlagDependencyUsagePlugin 插件中收集模塊的導(dǎo)出值的使用情況,并記錄到 exportInfo._usedInRuntime 集合中
  • 在 HarmonyExportXXXDependency.Template.apply 方法中根據(jù)導(dǎo)出值的使用情況生成不同的導(dǎo)出語句
  • 使用 DCE 工具刪除 Dead Code,實(shí)現(xiàn)完整的樹搖效果

上述實(shí)現(xiàn)原理對(duì)背景知識(shí)要求較高,建議讀者同步配合以下文檔食用:

  1. [萬字總結(jié)] 一文吃透 Webpack 核心原理
  2. 有點(diǎn)難的 webpack 知識(shí)點(diǎn):Dependency Graph 深度解析
  3. Webpack 原理系列六:徹底理解 Webpack 運(yùn)行時(shí)

三、最佳實(shí)踐

雖然 Webpack 自 2.x 開始就原生支持 Tree Shaking 功能,但受限于 JS 的動(dòng)態(tài)特性與模塊的復(fù)雜性,直至最新的 5.0 版本依然沒有解決許多代碼副作用帶來的問題,使得優(yōu)化效果并不如 Tree Shaking 原本設(shè)想的那么完美,所以需要使用者有意識(shí)地優(yōu)化代碼結(jié)構(gòu),或使用一些補(bǔ)丁技術(shù)幫助 Webpack 更精確地檢測(cè)無效代碼,完成 Tree Shaking 操作。

3.1 避免無意義的賦值

使用 Webpack 時(shí),需要有意識(shí)規(guī)避一些不必要的賦值操作,觀察下面這段示例代碼:

示例中,index.js 模塊引用了 bar.js 模塊的 foo 并賦值給 f 變量,但后續(xù)并沒有繼續(xù)用到 foo 或 f 變量,這種場(chǎng)景下 bar.js 模塊導(dǎo)出的 foo 值實(shí)際上并沒有被使用,理應(yīng)被刪除,但 Webpack 的 Tree Shaking 操作并沒有生效,產(chǎn)物中依然保留 foo 導(dǎo)出:

造成這一結(jié)果,淺層原因是 Webpack 的 Tree Shaking 邏輯停留在代碼靜態(tài)分析層面,只是淺顯地判斷:

  • 模塊導(dǎo)出變量是否被其它模塊引用
  • 引用模塊的主體代碼中有沒有出現(xiàn)這個(gè)變量

沒有進(jìn)一步,從語義上分析模塊導(dǎo)出值是不是真的被有效使用。

更深層次的原因則是 JavaScript 的賦值語句并不「純」,視具體場(chǎng)景有可能產(chǎn)生意料之外的副作用,例如:

  1. import { bar, foo } from "./bar"
  2.  
  3. let count = 0; 
  4.  
  5. const mock = {} 
  6.  
  7. Object.defineProperty(mock, 'f', { 
  8.     set(v) { 
  9.         mock._f = v; 
  10.         count += 1; 
  11.     } 
  12. }) 
  13.  
  14. mock.f = foo; 
  15.  
  16. console.log(count); 

示例中,對(duì) mock 對(duì)象施加的 Object.defineProperty 調(diào)用,導(dǎo)致 mock.f = foo 賦值語句對(duì) count 變量產(chǎn)生了副作用,這種場(chǎng)景下即使用復(fù)雜的動(dòng)態(tài)語義分析也很難在確保正確副作用的前提下,完美地 Shaking 掉所有無用的代碼枝葉。

因此,在使用 Webpack 時(shí)開發(fā)者需要有意識(shí)地規(guī)避這些無意義的重復(fù)賦值操作。

3.2 使用#pure標(biāo)注純函數(shù)調(diào)用

與賦值語句類似,JavaScript 中的函數(shù)調(diào)用語句也可能產(chǎn)生副作用,因此默認(rèn)情況下 Webpack 并不會(huì)對(duì)函數(shù)調(diào)用做 Tree Shaking 操作。不過,開發(fā)者可以在調(diào)用語句前添加 /*#__PURE__*/ 備注,明確告訴 Webpack 該次函數(shù)調(diào)用并不會(huì)對(duì)上下文環(huán)境產(chǎn)生副作用,例如:

示例中,foo('be retained') 調(diào)用沒有帶上 /*#__PURE__*/ 備注,代碼被保留;作為對(duì)比,foo('be removed') 帶上 Pure 聲明后則被 Tree Shaking 刪除。

3.3 禁止 Babel 轉(zhuǎn)譯模塊導(dǎo)入導(dǎo)出語句

Babel 是一個(gè)非常流行的 JavaScript 代碼轉(zhuǎn)換器,它能夠?qū)⒏甙姹镜?JS 代碼等價(jià)轉(zhuǎn)譯為兼容性更佳的低版本代碼,使得前端開發(fā)者能夠使用最新的語言特性開發(fā)出兼容舊版本瀏覽器的代碼。

但 Babel 提供的部分功能特性會(huì)致使 Tree Shaking 功能失效,例如 Babel 可以將 import/export 風(fēng)格的 ESM 語句等價(jià)轉(zhuǎn)譯為 CommonJS 風(fēng)格的模塊化語句,但該功能卻導(dǎo)致 Webpack 無法對(duì)轉(zhuǎn)譯后的模塊導(dǎo)入導(dǎo)出內(nèi)容做靜態(tài)分析,示例:

示例使用 babel-loader 處理 *.js 文件,并設(shè)置 Babel 配置項(xiàng) modules = 'commonjs',將模塊化方案從 ESM 轉(zhuǎn)譯到 CommonJS,導(dǎo)致轉(zhuǎn)譯代碼(右圖上一)沒有正確標(biāo)記出未被使用的導(dǎo)出值 foo。作為對(duì)比,右圖 2 為 modules = false 時(shí)打包的結(jié)果,此時(shí) foo 變量被正確標(biāo)記為 Dead Code。

所以,在 Webpack 中使用 babel-loader 時(shí),建議將 babel-preset-env 的 moduels 配置項(xiàng)設(shè)置為 false,關(guān)閉模塊導(dǎo)入導(dǎo)出語句的轉(zhuǎn)譯。

3.4 優(yōu)化導(dǎo)出值的粒度

Tree Shaking 邏輯作用在 ESM 的 export 語句上,因此對(duì)于下面這種導(dǎo)出場(chǎng)景:

  1. export default { 
  2.     bar: 'bar'
  3.     foo: 'foo' 

即使實(shí)際上只用到 default 導(dǎo)出值的其中一個(gè)屬性,整個(gè) default 對(duì)象依然會(huì)被完整保留。所以實(shí)際開發(fā)中,應(yīng)該盡量保持導(dǎo)出值顆粒度和原子性,上例代碼的優(yōu)化版本:

  1. const bar = 'bar' 
  2. const foo = 'foo' 
  3.  
  4. export { 
  5.     bar, 
  6.     foo 

3.5 使用支持 Tree Shaking 的包

如果可以的話,應(yīng)盡量使用支持 Tree Shaking 的 npm 包,例如:

  • 使用 lodash-es 替代 lodash ,或者使用 babel-plugin-lodash 實(shí)現(xiàn)類似效果

不過,并不是所有 npm 包都存在 Tree Shaking 的空間,諸如 React、Vue2 一類的框架原本已經(jīng)對(duì)生產(chǎn)版本做了足夠極致的優(yōu)化,此時(shí)業(yè)務(wù)代碼需要整個(gè)代碼包提供的完整功能,基本上不太需要進(jìn)行 Tree Shaking。

本文轉(zhuǎn)載自微信公眾號(hào)「Tecvan」

 

責(zé)任編輯:姜華 來源: Tecvan
相關(guān)推薦

2021-06-28 07:01:50

Webpack 前端Tree shakin

2022-02-10 14:23:16

WebpackJavaScript

2021-09-13 09:40:35

Webpack 前端HMR 原理

2021-05-31 05:36:43

WebpackJavaScript 前端

2021-06-28 05:59:17

Webpack 前端打包與工程化

2020-08-05 08:21:41

Webpack

2021-12-20 00:03:38

Webpack運(yùn)行機(jī)制

2021-12-16 22:02:28

webpack原理模塊化

2021-04-19 10:45:52

Webpack熱更新前端

2025-01-13 00:00:00

2022-08-26 13:24:03

version源碼sources

2019-01-23 08:48:50

跨域協(xié)議端口

2021-12-15 23:42:56

Webpack原理實(shí)踐

2020-12-03 10:40:23

webpack加載原理前端

2023-04-11 08:00:56

Redis類型編碼

2022-06-01 12:04:02

項(xiàng)目Webpack

2017-07-11 13:58:10

WebSocket

2021-12-24 08:01:44

Webpack優(yōu)化打包

2023-03-21 08:31:13

ReconcilerFiber架構(gòu)

2021-12-19 07:21:48

Webpack 前端插件機(jī)制
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)