Tree-Shaking性能優化實踐 - 原理篇

一. 什麼是Tree-shaking

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

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

圖1

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

二. tree-shaking的原理

圖2

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

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

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

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

圖3

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

?代碼不會被執行,不可到達

?代碼執行的結果不會被用到

?代碼只會影響死變數(只寫不讀)

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

圖4

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

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

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

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

lin-xi/treeshakinggithub.com圖標

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

圖5

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

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

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

圖6

中間是配置文件,右側是結果

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

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

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

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

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方法最終被消除。

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

先看看rollup的打包結果

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

再看看webpack的結果

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

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

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

再來看下類消除實驗

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

main.js

menu.js

rollup打包結果

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

webpack打包結果

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

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

what happend?

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

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

圖7

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

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

function Menu() {n}nnMenu.prototype.show = function() {n}nnArray.prototype.unique = function() {n // 將 array 中的重複元素去除n}nnexport default Menu;n

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

function Menu() {n}nnMenu.prototype.show = function() {n}nnvar a = Arr + aynvar bnif(a == Array) {n b = Arrayn} else {n b = Menun}nnb.prototype.unique = function() {n // 將 array 中的重複元素去除n}nnexport default Menu;n

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

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

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

tree-shaking對函數效果較好

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

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

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

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

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

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

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

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

說了這麼多,總結一下:

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

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

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

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

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

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

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

linxi:Tree-Shaking性能優化實踐 - 實踐篇zhuanlan.zhihu.com圖標

本文中示例代碼都能在我們的github中找到,歡迎戳?

lin-xi/treeshakinggithub.com圖標
推薦閱讀:

webpack技術講解及入門
webpack增量打包
vue.js不用yeoman怎麼搭建工程化結構?另外常用的vue的包又有哪些
require,import區別?
Webpack實戰-管理多個單頁應用

TAG:前端性能优化 | webpack | rollup |