日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
Webpack原理系列九:Tree-Shaking實(shí)現(xiàn)原理

一、什么是 Tree ShakingTree-Shaking

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

專注于為中小企業(yè)提供成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作、外貿(mào)營(yíng)銷網(wǎng)站建設(shè)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)成武免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上千余家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過(guò)網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。

Tree Shaking 較早前由 Rich Harris 在 Rollup 中率先實(shí)現(xiàn),Webpack 自 2.0 版本開(kāi)始接入,至今已經(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)化功能,可以通過(guò)如下方式實(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';
  4. }

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

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

所以,ESM 下模塊之間的依賴關(guān)系是高度確定的,與運(yùn)行狀態(tài)無(wú)關(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. // bar.js
  5. export const bar = 'bar';
  6. export const foo = 'foo';

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

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

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

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

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

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

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

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

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

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

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

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

1.將模塊的所有 ESM 導(dǎo)出語(yǔ)句轉(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. export default 'foo-bar'

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

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

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

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

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

參考資料:

[萬(wàn)字總結(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)出值有被其它模塊用到,哪些沒(méi)有,這一過(guò)程發(fā)生在 Seal 階段,主流程:

  1. 觸發(fā) compilation.hooks.optimizeDependencies 鉤子,開(kāi)始執(zhí)行 FlagDependencyUsagePlugin 插件邏輯
  2. 在 FlagDependencyUsagePlugin 插件中,從 entry 開(kāi)始逐步遍歷 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é)束

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

2.3 生成代碼

經(jīng)過(guò)前面的收集與標(biāo)記步驟后,Webpack 已經(jīng)在 ModuleGraph 體系中清楚地記錄了每個(gè)模塊都導(dǎo)出了哪些值,每個(gè)導(dǎo)出值又沒(méi)那塊模塊所使用。接下來(lái),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 則僅僅保留了定義語(yǔ)句,沒(méi)有在 chunk 中生成對(duì)應(yīng)的 export。

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

這一段生成邏輯均由導(dǎo)出語(yǔ)句對(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)出語(yǔ)句。

2.4 刪除 Dead Code

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

在此之后,將由 Terser、UglifyJS 等 DCE 工具“搖”掉這部分無(wú)效代碼,構(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)出語(yǔ)句
  • 使用 DCE 工具刪除 Dead Code,實(shí)現(xiàn)完整的樹(shù)搖效果

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

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

三、最佳實(shí)踐

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

3.1 避免無(wú)意義的賦值

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

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

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

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

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

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

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

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

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

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

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

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

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

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

但 Babel 提供的部分功能特性會(huì)致使 Tree Shaking 功能失效,例如 Babel 可以將 import/export 風(fēng)格的 ESM 語(yǔ)句等價(jià)轉(zhuǎn)譯為 CommonJS 風(fēng)格的模塊化語(yǔ)句,但該功能卻導(dǎo)致 Webpack 無(wú)法對(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)譯代碼(右圖上一)沒(méi)有正確標(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)出語(yǔ)句的轉(zhuǎn)譯。

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

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

 
 
 
 
  1. export default {
  2.     bar: 'bar',
  3.     foo: 'foo'
  4. }

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

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

3.5 使用支持 Tree Shaking 的包

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

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

不過(guò),并不是所有 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」


本文標(biāo)題:Webpack原理系列九:Tree-Shaking實(shí)現(xiàn)原理
本文鏈接:http://m.5511xx.com/article/djsdois.html