Tree-Shaking效能優化實踐 – 原理篇

百度外賣大前端技術團隊發表於2019-03-04

一. 什麼是Tree-shaking

Tree-Shaking效能優化實踐 – 原理篇

先來看一下Tree-shaking原始的本意

Tree-Shaking效能優化實踐 – 原理篇

上圖形象的解釋了Tree-shaking 的本意,本文所說的前端中的tree-shaking可以理解為通過工具”搖”我們的JS檔案,將其中用不到的程式碼”搖”掉,是一個效能優化的範疇。具體來說,在 webpack 專案中,有一個入口檔案,相當於一棵樹的主幹,入口檔案有很多依賴的模組,相當於樹枝。實際情況中,雖然依賴了某個模組,但其實只使用其中的某些功能。通過 tree-shaking,將沒有使用的模組搖掉,這樣來達到刪除無用程式碼的目的。

Tree-Shaking效能優化實踐 – 原理篇

Tree-shaking 較早由 Rich_Harris 的 rollup 實現,後來,webpack2 也增加了tree-shaking 的功能。其實在更早,google closure compiler 也做過類似的事情。三個工具的效果和使用各不相同,使用方法可以通過官網文件去了解,三者的效果對比,後文會詳細介紹。

二. tree-shaking的原理

Tree-Shaking效能優化實踐 – 原理篇

Tree-shaking的本質是消除無用的js程式碼。無用程式碼消除在廣泛存在於傳統的程式語言編譯器中,編譯器可以判斷出某些程式碼根本不影響輸出,然後消除這些程式碼,這個稱之為DCE(dead code elimination)。

Tree-shaking 是 DCE 的一種新的實現,Javascript同傳統的程式語言不同的是,javascript絕大多數情況需要通過網路進行載入,然後執行,載入的檔案大小越小,整體執行時間更短,所以去除無用程式碼以減少檔案體積,對javascript來說更有意義。

Tree-shaking 和傳統的 DCE的方法又不太一樣,傳統的DCE 消滅不可能執行的程式碼,而Tree-shaking 更關注宇消除沒有用到的程式碼。下面詳細介紹一下DCE和Tree-shaking。

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

Tree-Shaking效能優化實踐 – 原理篇

Dead Code 一般具有以下幾個特徵

•程式碼不會被執行,不可到達

•程式碼執行的結果不會被用到

•程式碼只會影響死變數(只寫不讀)

下面紅框標示的程式碼就屬於死碼,滿足以上特徵

Tree-Shaking效能優化實踐 – 原理篇
圖4

傳統編譯型的語言中,都是由編譯器將Dead Code從AST(抽象語法樹)中刪除,那javascript中是由誰做DCE呢?

首先肯定不是瀏覽器做DCE,因為當我們的程式碼送到瀏覽器,那還談什麼消除無法執行的程式碼來優化呢,所以肯定是送到瀏覽器之前的步驟進行優化。

其實也不是上面提到的三個工具,rollup,webpack,cc做的,而是著名的程式碼壓縮優化工具uglify,uglify完成了javascript的DCE,下面通過一個實驗來驗證一下。

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

github.com/lin-xi/tree…

分別用rollup和webpack將圖4中的程式碼進行打包

Tree-Shaking效能優化實踐 – 原理篇
圖5

中間是rollup打包的結果,右邊是webpack打包的結果

可以發現,rollup將無用的程式碼foo函式和unused函式消除了,但是仍然保留了不會執行到的程式碼,而webpack完整的保留了所有的無用程式碼和不會執行到的程式碼。

分別用rollup + uglify和 webpack + uglify 將圖4中的程式碼進行打包

Tree-Shaking效能優化實踐 – 原理篇
圖6

中間是配置檔案,右側是結果

可以看到右側最終打包結果中都去除了無法執行到的程式碼,結果符合我們的預期。

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

前面提到了tree-shaking更關注於無用模組的消除,消除那些引用了但並沒有被使用的模組。

先思考一個問題,為什麼tree-shaking是最近幾年流行起來了?而前端模組化概念已經有很多年曆史了,其實tree-shaking的消除原理是依賴於ES6的模組特性。

Tree-Shaking效能優化實踐 – 原理篇

ES6 module 特點:

  • 只能作為模組頂層的語句出現
  • import 的模組名只能是字串常量
  • import binding 是 immutable的

ES6模組依賴關係是確定的,和執行時的狀態無關,可以進行可靠的靜態分析,這就是tree-shaking的基礎。

所謂靜態分析就是不執行程式碼,從字面量上對程式碼進行分析,ES6之前的模組化,比如我們可以動態require一個模組,只有執行後才知道引用的什麼模組,這個就不能通過靜態分析去做優化。

這是 ES6 modules 在設計時的一個重要考量,也是為什麼沒有直接採用 CommonJS,正是基於這個基礎上,才使得 tree-shaking 成為可能,這也是為什麼 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

我們還是通過例子來詳細瞭解一下

程式導向程式設計函式和麵向物件程式設計是javascript最常用的程式設計模式和程式碼組織方式,從這兩個方面來實驗:

  • 函式消除實驗
  • 類消除實驗

先看下函式消除實驗

utils中get方法沒有被使用到,我們期望的是get方法最終被消除。

Tree-Shaking效能優化實踐 – 原理篇

注意,uglify目前不會跨檔案去做DCE,所以上面這種情況,uglify是不能優化的。

先看看rollup的打包結果

Tree-Shaking效能優化實踐 – 原理篇

完全符合預期,最終結果中沒有get方法

再看看webpack的結果

Tree-Shaking效能優化實踐 – 原理篇

也符合預期,最終結果中沒有get方法

可以看到rollup打包的結果比webpack更優化

函式消除實驗中,rollup和webpack都通過,符合預期

再來看下類消除實驗

增加了對menu.js的引用,但其實程式碼中並沒有用到menu的任何方法和變數,所以我們的期望是,最終程式碼中menu.js裡的內容被消除

Tree-Shaking效能優化實踐 – 原理篇
main.js
Tree-Shaking效能優化實踐 – 原理篇
menu.js

rollup打包結果

Tree-Shaking效能優化實踐 – 原理篇

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

webpack打包結果

Tree-Shaking效能優化實踐 – 原理篇

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

類消除實驗中,rollup,webpack 全軍覆沒,都沒有達到預期

Tree-Shaking效能優化實踐 – 原理篇
what happend?

這跟我們想象的完全不一樣啊?為什麼呢?無用的類不能消除,這還能叫做tree-shaking嗎?我當時一度懷疑自己的demo有問題,後來各種網上搜尋,才明白demo沒有錯。

下面摘取了rollup核心貢獻者的的一些回答:

Tree-Shaking效能優化實踐 – 原理篇
圖7
  • rollup只處理函式和頂層的import/export變數,不能把沒用到的類的方法消除掉
  • javascript動態語言的特性使得靜態分析比較困難
  • 圖7下部分的程式碼就是副作用的一個例子,如果靜態分析的時候刪除裡run或者jump,程式執行時就可能報錯,那就本末倒置了,我們的目的是優化,肯定不能影響執行

再舉個例子說明下為什麼不能消除menu.js,比如下面這個場景

function Menu() {
}

Menu.prototype.show = function() {
}

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

export default Menu;
複製程式碼

如果刪除裡menu.js,那對Array的擴充套件也會被刪除,就會影響功能。那也許你會問,難道rollup,webpack不能區分是定義Menu的proptotype 還是定義Array的proptotype嗎?當然如果程式碼寫成上面這種形式是可以區分的,如果我寫成這樣呢?

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 中的重複元素去除
}

export default Menu;
複製程式碼

這種程式碼,靜態分析是分析不了的,就算能靜態分析程式碼,想要正確完全的分析也比較困難。

更多關於副作用的討論,可以看這個

圖示

Tree shaking class methods · Issue #349 · rollup/rollupgithub.com

Tree-Shaking效能優化實踐 – 原理篇

tree-shaking對函式效果較好

函式的副作用相對較少,頂層函式相對來說更容易分析,加上babel預設都是”use strict”嚴格模式,減少頂層函式的動態訪問的方式,也更容易分析

我們開始說的三個工具,rollup和webpack表現不理想,那closure compiler又如何呢?

將示例中的程式碼用cc打包後得到的結果如下:

Tree-Shaking效能優化實踐 – 原理篇

天啊,這不就是我們要的結果嗎?完美消除所有無用程式碼的結果,輸出的結果非常性感

closure compiler, tree-shaking的結果完美!

可是不能高興得太早,能得到這麼完美結果是需要條件的,那就是cc的侵入式約束規範。必須在程式碼裡新增這樣的程式碼,看紅線框標示的

Tree-Shaking效能優化實踐 – 原理篇

google定義一整套註解規範Annotating JavaScript for the Closure Compiler,想更多瞭解的,可以去看下官網。

侵入式這個就讓人很不爽,google Closure Compiler是java寫的,和我們基於node的各種構建庫不可能相容(不過目前好像已經有nodejs版 Closure Compiler),Closure Compiler使用起來也比較麻煩,所以雖然效果很贊,但比較難以應用到專案中,遷移成本較大。

說了這麼多,總結一下:

三大工具的tree-shaking對於無用程式碼,無用模組的消除,都是有限的,有條件的。closure compiler是最好的,但與我們日常的基於node的開發流很難相容。

Tree-Shaking效能優化實踐 – 原理篇

tree-shaking對web意義重大,是一個極致優化的理想世界,是前端進化的又一個終極理想。

理想是美好的,但目前還處在發展階段,還比較困難,有各個方面的,甚至有目前看來無法解

決的問題,但還是應該相信新技術能帶來更好的前端世界。

優化是一種態度,不因小而不為,不因艱而不攻。

知識有限,如果錯誤,請不惜指正,謝謝

下一篇將繼續介紹 Tree-Shaking效能優化實踐 – 實踐篇

圖示

本文中示例程式碼都能在我們的github中找到,歡迎戳❤

圖示

lin-xi/treeshakinggithub.com

相關文章