Tree-Shaking性能優(yōu)化實(shí)踐 - 原理篇

一. 什么是Tree-shaking

[圖片上傳中...(image-1e0c64-1566907465560-23)]

先來(lái)看一下Tree-shaking原始的本意

[圖片上傳中...(image-acca4f-1566907465558-2)]

上圖形象的解釋了Tree-shaking 的本意,本文所說(shuō)的前端中的tree-shaking可以理解為通過(guò)工具"搖"我們的JS文件,將其中用不到的代碼"搖"掉,是一個(gè)性能優(yōu)化的范疇。具體來(lái)說(shuō),在 webpack 項(xiàng)目中,有一個(gè)入口文件,相當(dāng)于一棵樹(shù)的主干,入口文件有很多依賴的模塊,相當(dāng)于樹(shù)枝。實(shí)際情況中,雖然依賴了某個(gè)模塊,但其實(shí)只使用其中的某些功能。通過(guò) tree-shaking,將沒(méi)有使用的模塊搖掉,這樣來(lái)達(dá)到刪除無(wú)用代碼的目的。

[圖片上傳中...(image-7d6844-1566907465560-22)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖1</figcaption>

Tree-shaking 較早由 Rich_Harris 的 rollup 實(shí)現(xiàn),后來(lái),webpack2 也增加了tree-shaking 的功能。其實(shí)在更早,google closure compiler 也做過(guò)類似的事情。三個(gè)工具的效果和使用各不相同,使用方法可以通過(guò)官網(wǎng)文檔去了解,三者的效果對(duì)比,后文會(huì)詳細(xì)介紹。

二. tree-shaking的原理

[圖片上傳中...(image-169f96-1566907465560-21)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖2</figcaption>

Tree-shaking的本質(zhì)是消除無(wú)用的js代碼。無(wú)用代碼消除在廣泛存在于傳統(tǒng)的編程語(yǔ)言編譯器中,編譯器可以判斷出某些代碼根本不影響輸出,然后消除這些代碼,這個(gè)稱之為DCE(dead code elimination)。

Tree-shaking 是 DCE 的一種新的實(shí)現(xiàn),Javascript同傳統(tǒng)的編程語(yǔ)言不同的是,javascript絕大多數(shù)情況需要通過(guò)網(wǎng)絡(luò)進(jìn)行加載,然后執(zhí)行,加載的文件大小越小,整體執(zhí)行時(shí)間更短,所以去除無(wú)用代碼以減少文件體積,對(duì)javascript來(lái)說(shuō)更有意義。

Tree-shaking 和傳統(tǒng)的 DCE的方法又不太一樣,傳統(tǒng)的DCE 消滅不可能執(zhí)行的代碼,而Tree-shaking 更關(guān)注宇消除沒(méi)有用到的代碼。下面詳細(xì)介紹一下DCE和Tree-shaking。

(1)先來(lái)看一下DCE消除大法

[圖片上傳中...(image-b316eb-1566907465560-20)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖3</figcaption>

Dead Code 一般具有以下幾個(gè)特征

?代碼不會(huì)被執(zhí)行,不可到達(dá)

?代碼執(zhí)行的結(jié)果不會(huì)被用到

?代碼只會(huì)影響死變量(只寫不讀)

下面紅框標(biāo)示的代碼就屬于死碼,滿足以上特征

[圖片上傳中...(image-e51151-1566907465560-19)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖4</figcaption>

傳統(tǒng)編譯型的語(yǔ)言中,都是由編譯器將Dead Code從AST(抽象語(yǔ)法樹(shù))中刪除,那javascript中是由誰(shuí)做DCE呢?

首先肯定不是瀏覽器做DCE,因?yàn)楫?dāng)我們的代碼送到瀏覽器,那還談什么消除無(wú)法執(zhí)行的代碼來(lái)優(yōu)化呢,所以肯定是送到瀏覽器之前的步驟進(jìn)行優(yōu)化。

其實(shí)也不是上面提到的三個(gè)工具,rollup,webpack,cc做的,而是著名的代碼壓縮優(yōu)化工具uglify,uglify完成了javascript的DCE,下面通過(guò)一個(gè)實(shí)驗(yàn)來(lái)驗(yàn)證一下。

以下所有的示例代碼都能在我們的github中找到,歡迎戳?

lin-xi/treeshaking?github.com[圖片上傳中...(image-16d167-1566907465558-1)]

分別用rollup和webpack將圖4中的代碼進(jìn)行打包

[圖片上傳中...(image-11f2b9-1566907465560-18)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖5</figcaption>

中間是rollup打包的結(jié)果,右邊是webpack打包的結(jié)果

可以發(fā)現(xiàn),rollup將無(wú)用的代碼foo函數(shù)和unused函數(shù)消除了,但是仍然保留了不會(huì)執(zhí)行到的代碼,而webpack完整的保留了所有的無(wú)用代碼和不會(huì)執(zhí)行到的代碼。

分別用rollup + uglify和 webpack + uglify 將圖4中的代碼進(jìn)行打包

[圖片上傳中...(image-71cf0b-1566907465560-17)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖6</figcaption>

中間是配置文件,右側(cè)是結(jié)果

可以看到右側(cè)最終打包結(jié)果中都去除了無(wú)法執(zhí)行到的代碼,結(jié)果符合我們的預(yù)期。

(2) 再來(lái)看一下Tree-shaking消除大法

前面提到了tree-shaking更關(guān)注于無(wú)用模塊的消除,消除那些引用了但并沒(méi)有被使用的模塊。

先思考一個(gè)問(wèn)題,為什么tree-shaking是最近幾年流行起來(lái)了?而前端模塊化概念已經(jīng)有很多年歷史了,其實(shí)tree-shaking的消除原理是依賴于ES6的模塊特性。

[圖片上傳中...(image-113e4-1566907465559-16)]

ES6 module 特點(diǎn):

  • 只能作為模塊頂層的語(yǔ)句出現(xiàn)
  • import 的模塊名只能是字符串常量
  • import binding 是 immutable的

ES6模塊依賴關(guān)系是確定的,和運(yùn)行時(shí)的狀態(tài)無(wú)關(guān),可以進(jìn)行可靠的靜態(tài)分析,這就是tree-shaking的基礎(chǔ)。

所謂靜態(tài)分析就是不執(zhí)行代碼,從字面量上對(duì)代碼進(jìn)行分析,ES6之前的模塊化,比如我們可以動(dòng)態(tài)require一個(gè)模塊,只有執(zhí)行后才知道引用的什么模塊,這個(gè)就不能通過(guò)靜態(tài)分析去做優(yōu)化。

這是 ES6 modules 在設(shè)計(jì)時(shí)的一個(gè)重要考量,也是為什么沒(méi)有直接采用 CommonJS,正是基于這個(gè)基礎(chǔ)上,才使得 tree-shaking 成為可能,這也是為什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

我們還是通過(guò)例子來(lái)詳細(xì)了解一下

面向過(guò)程編程函數(shù)和面向?qū)ο缶幊淌莏avascript最常用的編程模式和代碼組織方式,從這兩個(gè)方面來(lái)實(shí)驗(yàn):

  • 函數(shù)消除實(shí)驗(yàn)
  • 類消除實(shí)驗(yàn)

先看下函數(shù)消除實(shí)驗(yàn)

utils中g(shù)et方法沒(méi)有被使用到,我們期望的是get方法最終被消除。

[圖片上傳中...(image-9152cd-1566907465559-15)]

注意,uglify目前不會(huì)跨文件去做DCE,所以上面這種情況,uglify是不能優(yōu)化的。

先看看rollup的打包結(jié)果

[圖片上傳中...(image-3a5dd3-1566907465559-14)]

完全符合預(yù)期,最終結(jié)果中沒(méi)有g(shù)et方法

再看看webpack的結(jié)果

[圖片上傳中...(image-72c1c5-1566907465559-13)]

也符合預(yù)期,最終結(jié)果中沒(méi)有g(shù)et方法

可以看到rollup打包的結(jié)果比webpack更優(yōu)化

函數(shù)消除實(shí)驗(yàn)中,rollup和webpack都通過(guò),符合預(yù)期

再來(lái)看下類消除實(shí)驗(yàn)

增加了對(duì)menu.js的引用,但其實(shí)代碼中并沒(méi)有用到menu的任何方法和變量,所以我們的期望是,最終代碼中menu.js里的內(nèi)容被消除

[圖片上傳中...(image-512eaa-1566907465559-12)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">main.js</figcaption>

[圖片上傳中...(image-b6892b-1566907465559-11)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">menu.js</figcaption>

rollup打包結(jié)果

[圖片上傳中...(image-c1997e-1566907465559-10)]

包中竟然包含了menu.js的全部代碼

webpack打包結(jié)果

[圖片上傳中...(image-2044e3-1566907465559-9)]

包中竟然也包含了menu.js的全部代碼

類消除實(shí)驗(yàn)中,rollup,webpack 全軍覆沒(méi),都沒(méi)有達(dá)到預(yù)期

[圖片上傳中...(image-5ad845-1566907465559-8)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">what happend?</figcaption>

這跟我們想象的完全不一樣???為什么呢?無(wú)用的類不能消除,這還能叫做tree-shaking嗎?我當(dāng)時(shí)一度懷疑自己的demo有問(wèn)題,后來(lái)各種網(wǎng)上搜索,才明白demo沒(méi)有錯(cuò)。

下面摘取了rollup核心貢獻(xiàn)者的的一些回答:

[圖片上傳中...(image-11836-1566907465559-7)]

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖7</figcaption>

  • rollup只處理函數(shù)和頂層的import/export變量,不能把沒(méi)用到的類的方法消除掉
  • javascript動(dòng)態(tài)語(yǔ)言的特性使得靜態(tài)分析比較困難
  • 圖7下部分的代碼就是副作用的一個(gè)例子,如果靜態(tài)分析的時(shí)候刪除里run或者jump,程序運(yùn)行時(shí)就可能報(bào)錯(cuò),那就本末倒置了,我們的目的是優(yōu)化,肯定不能影響執(zhí)行

再舉個(gè)例子說(shuō)明下為什么不能消除menu.js,比如下面這個(gè)場(chǎng)景

function Menu() {
}

Menu.prototype.show = function() {
}

Array.prototype.unique = function() {
    // 將 array 中的重復(fù)元素去除
}

export default Menu;

如果刪除里menu.js,那對(duì)Array的擴(kuò)展也會(huì)被刪除,就會(huì)影響功能。那也許你會(huì)問(wèn),難道rollup,webpack不能區(qū)分是定義Menu的proptotype 還是定義Array的proptotype嗎?當(dāng)然如果代碼寫成上面這種形式是可以區(qū)分的,如果我寫成這樣呢?

function Menu() {
}

Menu.prototype.show = function() {
}

var a = 'Arr' + 'ay'
var b
if(a == 'Array') {
    b = Array
} else {
    b = Menu
}

b.prototype.unique = function() {
    // 將 array 中的重復(fù)元素去除
}

export default Menu;

這種代碼,靜態(tài)分析是分析不了的,就算能靜態(tài)分析代碼,想要正確完全的分析也比較困難。

更多關(guān)于副作用的討論,可以看這個(gè)

Tree shaking class methods · Issue #349 · rollup/rollup?github.com[圖片上傳中...(image-e2c32-1566907465558-0)] [圖片上傳中...(image-5cf21-1566907465559-6)]

tree-shaking對(duì)函數(shù)效果較好

函數(shù)的副作用相對(duì)較少,頂層函數(shù)相對(duì)來(lái)說(shuō)更容易分析,加上babel默認(rèn)都是"use strict"嚴(yán)格模式,減少頂層函數(shù)的動(dòng)態(tài)訪問(wèn)的方式,也更容易分析

我們開(kāi)始說(shuō)的三個(gè)工具,rollup和webpack表現(xiàn)不理想,那closure compiler又如何呢?

將示例中的代碼用cc打包后得到的結(jié)果如下:

[圖片上傳中...(image-cbb2de-1566907465559-5)]

天啊,這不就是我們要的結(jié)果嗎?完美消除所有無(wú)用代碼的結(jié)果,輸出的結(jié)果非常性感

closure compiler, tree-shaking的結(jié)果完美!

可是不能高興得太早,能得到這么完美結(jié)果是需要條件的,那就是cc的侵入式約束規(guī)范。必須在代碼里添加這樣的代碼,看紅線框標(biāo)示的

[圖片上傳中...(image-7ab23c-1566907465559-4)]

google定義一整套注解規(guī)范Annotating JavaScript for the Closure Compiler,想更多了解的,可以去看下官網(wǎng)。

侵入式這個(gè)就讓人很不爽,google Closure Compiler是java寫的,和我們基于node的各種構(gòu)建庫(kù)不可能兼容(不過(guò)目前好像已經(jīng)有nodejs版 Closure Compiler),Closure Compiler使用起來(lái)也比較麻煩,所以雖然效果很贊,但比較難以應(yīng)用到項(xiàng)目中,遷移成本較大。

說(shuō)了這么多,總結(jié)一下:

三大工具的tree-shaking對(duì)于無(wú)用代碼,無(wú)用模塊的消除,都是有限的,有條件的。closure compiler是最好的,但與我們?nèi)粘5幕趎ode的開(kāi)發(fā)流很難兼容。

[圖片上傳中...(image-755b8c-1566907465559-3)]

tree-shaking對(duì)web意義重大,是一個(gè)極致優(yōu)化的理想世界,是前端進(jìn)化的又一個(gè)終極理想。

理想是美好的,但目前還處在發(fā)展階段,還比較困難,有各個(gè)方面的,甚至有目前看來(lái)無(wú)法解

決的問(wèn)題,但還是應(yīng)該相信新技術(shù)能帶來(lái)更好的前端世界。

但優(yōu)化是一種態(tài)度,不因小而不為,不因艱而不攻。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容