原來項目打包也有這么技巧 - 淺談 Tree Shaking 機(jī)制
前言
身為一位前端工程師或多或少都有聽過 Webpack 這套前端打包工具吧,為了讓最終打包的檔案不會過于龐大,Webpack 可是下了非常多的苦功,例如:利用 Code Splitting 產(chǎn)出一個又一個的 chunk 讓網(wǎng)頁不會一次載入一份很大 JS包。
然而今天的文章其實(shí)不是要講 Code Splitting,而是要講一個比較深入的原理:Tree Shaking。
什么是 Tree Shaking?
什么是 Tree Shaking?Tree Shaking 就字面上翻譯來看就是搖晃樹木,在 Webpack 的世界中我們通常都會設(shè)定一個 Entry Points 來告訴 webpack 要從哪個文件開始往其他文件進(jìn)行打包,如果用 Tree 的概念來看就是一個主干配上很多的樹枝。
Dynamic Language & Static Language
接下來講個跟 Tree Shaking 比較無關(guān)的小知識,但這個小觀念可以幫助我們了解為何要在 JavaScript 上執(zhí)行 Tree Shaking 并不是我們想像中的那麼容易。
接下來講個跟 Tree Shaking 比較無關(guān)的小知識,但這個小觀念可以幫助我們了解為何要在 JavaScript 上執(zhí)行 Tree Shaking 并在程式語言中有分為 動態(tài)語言(Dynamic Language) 以及 靜態(tài)語言(Static Language),被歸類在 Dynamic Language 中比較常見的有 JavaScript、PHP、Python 等語不是我們想像中的那么容易。
在編程語言中有分為 動態(tài)語言(Dynamic Language) 以及 **靜態(tài)語言(Static Language)**,被歸類在 Dynamic Language 中比較常見的有 JavaScript、PHP、Python 等語言,至于被歸類在 Static Language 比較常見的有 C++、Java 等語言。
在 Dynamic Language 中由于我們可以動態(tài)的載入非常多東西,例如 function、object 等,對于 Tree Shaking 來說這種會動態(tài)載入的東西實(shí)在是太難捉摸了,這也讓 Dynamic Language 的 Tree Shaking 很難達(dá)到最完美。
Dead Code Elimination
在開始講 Tree Shaking 原理之前必須要了解一個技術(shù):死碼刪除(Dea誒 Code Elimination)。
在 ??compiler?
? 的領(lǐng)域中,為了達(dá)到執(zhí)行時間的優(yōu)化,在代碼編譯的過程中 compiler 會將對于最終結(jié)果沒有影響到的代碼刪除,進(jìn)而達(dá)到執(zhí)行時間的優(yōu)化,這段過程稱之為 Dead Code Elimination。
乍看之下 Dead Code Elimination 在做的事情好像就是 Tree Shaking 要做到的事情,就是要刪除無用的代碼,但兩者其實(shí)還是有著些微的差距,接下來就要講講 Tree Shaking 的原理。
Tree Shaking 原理
Tree Shaking 其實(shí)是 Dead Code Elimination 的一種新的實(shí)現(xiàn)原理,在上面的 Dynamic Language 的觀念中提到 Dynamic Language 的特性就是可以動態(tài)載入任何東西,因為這個特性讓 Dead Code Elimination 相當(dāng)難實(shí)現(xiàn),因為 complier 永遠(yuǎn)不知道到底哪些程代碼是對最終結(jié)果不會有影響的。
所以 Tree Shaking 其實(shí)要做到的不會像 Dead Code Elimination 那樣死板板的要刪除對結(jié)果不會有影響的程式碼,而是要保留會需要用到的代碼,這樣也可以達(dá)到類似 Dead Code Elimination 的效果,只是兩者的原理還是有一些差異,而這就是 Tree Shaking 的原理。
ES6 module v.s commonJS
上面提到 Tree Shaking 的原理最主要的目的就是要保留會需要用到的代碼,而這點(diǎn)在早期的 JavaScript 其實(shí)是無法實(shí)現(xiàn)的,但是在 ES6 誕生后有一個非常重要的概念叫:ES6 modules。
由于 ES6 modules 的誕生,我們可以在每個文件的最上方先引用即將會需要用到的東西,所以這些 bunbler 就可以藉由這些?? import file?
? 很快速的知道可以保留哪些文件,進(jìn)而達(dá)到 Tree Shaking 的效果。
這時候讀者可能會有另一個問題了,在 ES6 module 還沒誕生以前我們也可以利用 commonJS 來進(jìn)行 module 的導(dǎo)入,為什麼 ES6 module 可以做到 Tree Shaking 可是 commonJS 無法呢?
其實(shí)是因為 ES6 module 有著非常多的特性,讓 bundler 可以針對這些特性來進(jìn)行靜態(tài)的分析:
- module 必須要在頂層被 import。
- module 內(nèi)部會自動被定義為 strict mode。
- module name 不能動態(tài)改變。
- module 內(nèi)容為 immutable 無法在其他文件中被動態(tài)新增或刪除內(nèi)容。
因為這些強(qiáng)限制在,所以 ES6 module 就可以讓 bundler 做到 Tree Shaking 的效果,而 commonJS 則無法達(dá)到此點(diǎn)。
改善 import 與 export 方式
我們都知道 ES6 modules 的 export 方式有分 ??named export?
?? 以及?? default export?
?,這兩種方法適用于不同的使用場景,也會對 Tree Shaking 后的文件內(nèi)容有著非常大的差別。
default export
named export
乍看之下 ??default export?
?? 跟?? named export?
? 在寫法上好像沒什麼太大的差別(除了直接在項目前面加上 export 的寫法比較不一樣外),最終都是需要用一個物件來包裝輸出,但兩者在 Tree Shaking 后的結(jié)果可是有著蠻大的差別,接下來就看來一下 Tree Shaking 過后的結(jié)果吧!
default export 經(jīng)由 Tree Shaking 后的結(jié)果:
named export 經(jīng)由 Tree Shaking 后的結(jié)果:
可以看到上面兩張圖,雖然 Tree Shaking 都有把 multiply 這個 function 移除了,可是 ??default export?
?? 相較于 ??named export?
?? 還是新增了不少變量來處理 ??function parameter?
?,這樣就不是一個完美的性能優(yōu)化。
所以假如讀者在開發(fā)時確定一個文件會需要同時輸出很多項目,不管是對象也好函數(shù)也罷,這時候都建議用 ??named export?
? 的方式進(jìn)行輸出這樣才能達(dá)到最好的性能優(yōu)化。
改善第三方組件的 import 方式
最后再來看一下 import 第三方組件的最佳方式,在前端開發(fā)的過程中為了不要重復(fù)照輪子很多時候都會使用大神所開發(fā)好的第三方組件來加速開發(fā),但第三方組件的 ??import?
? 方式其實(shí)也會影響到最終的 bundle size。
接下將以 ant design 這套 UI library 來進(jìn)行說明。
首先是利用官方文檔的說明來進(jìn)行 import,其實(shí) antd 本身就有針對其 module 進(jìn)行 Tree Shaking 的性能優(yōu)化,所以我們原則上是可以放心的使用官方文檔的教學(xué)進(jìn)行 ??import?
?? 的,接下來我們利用 ??webpack-bundle-analyzer?
? 來進(jìn)行檔案分析。
可以發(fā)現(xiàn) antd 的文件大小高達(dá) ??842.15KB?
??,而且裡面還跑出了許多跟 Button 無關(guān)的 component 文件,這顯然是一個不好的 ??import?
?? 方式,沒想到照著官文檔的方式進(jìn)行 ??import?
? 也沒辦法達(dá)到最好的性能優(yōu)化。
但這其實(shí)也不是 antd 的錯, antd 本身就有做好 Tree Shaking 的動作,詳細(xì)的說明可以參考 antd 的官方文件,但是這邊的事例故意沒有在項目的 bundler 設(shè)定檔中開啟 Tree Shaking 的功能,進(jìn)而導(dǎo)致 antd 的 Tree Shaking 失效。
雖然 bundler 沒有開啟 Tree Shaking 功能讓整體的 bundle size 過大,但我們其實(shí)也可以自己手動做這件事,這時候只要我們改成從 antd 的 es folder 進(jìn)行元件的單獨(dú) ??import?
? 就可以讓最終的 bundle size 差非常多,寫法如下。
接著我們一樣使用 webpack-bundle-analyzer 來進(jìn)行項目分析。
可以發(fā)現(xiàn)整個 antd 的文件大小少了非常多,只剩下 ??74.8KB?
?? 而且與 Button 無關(guān)的其他 component 都沒出現(xiàn)了,所以同一種第三方組件不同的 import 方式真的會讓整體的性能差距非常大,這個就是比較好的第三方組件 ??import?
? 方式。
package.json 中的 sideEffects
在 Webpack 的 Tree Shaking 配置中,有一個可以在 ??package.json?
?? 中配置的叫 ??sideEffects?
??,這個 ??sideEffects?
? 的配置主要是讓 Webpack 這種 bundler 知道此項目是否可以做 Tree Shaking 的動作。
假如設(shè)定為 ??false?
?? 就代表可以將所有的文件進(jìn)行 Tree Shaking,若讀者知道有哪些檔案是不能做 Tree Shaking 的,這時候只要在 ??sideEffects?
? 內(nèi)用一個數(shù)組將不能做 Tree Shaking 的文件路徑寫上去,這時候 bundler 就只會針對這個數(shù)組以外的文件進(jìn)行 Tree Shaking。
Webpack 中的 usedExports
在 Webpack 的官方文件中要達(dá)到 Tree Shaking 的效果除了在 ??package.json?
?? 中加上 ??sideEffects?
?? 外,還可以使用 ??usedExports?
?。
在官方文件中有這麼一段說明:
如果說 sideEffects 在做的事情是把不能做 Tree Shaking 的樹枝移除,那 ??usedExports?
? 在做的事情就是把樹枝上沒有用到的樹葉移除,所以 usedExports 其實(shí)才是在做真正的 Tree Shaking。
useExports 利用 terser 這套工具進(jìn)行項目的 side effects 偵測,假如打包過程中發(fā)現(xiàn)此弎既沒有 side effects 且某些代碼又沒有被引用到,則該代碼就會在之后的 ??uglify?
? 被移除,藉此達(dá)到真正的 Tree Shaking 效果。
而 usedExports 的設(shè)定方式也非常簡單,只要在 Webpack 的配置文件中,在 ??optimization?
?? 內(nèi)加上?? usedExports: true?
? 這時候就可以將 usedExports 的功能打開,寫法如下:
小結(jié)
今天介紹了 Tree Shaking 的相關(guān)基本觀念,雖然說身為一位前端工程師不一定要懂這個概念,畢竟現(xiàn)在很多主流的框架都已經(jīng)先把 bundler 的相關(guān) ??config?
? 都寫好了,但了解這些工具背后在做的事情也能幫助到自己在開發(fā)時可以稍微省思一下要如何改良自己的代碼,進(jìn)而提升整體的打包后的性能。
像是上面提到的 import 與 export 方式,引用第三方組件時可以如何引用達(dá)到最小的 bundle
size,有了這些概念在開發(fā)時就可以提升整體的性能 ,所以筆者也建議目前正在學(xué)習(xí)網(wǎng)頁開發(fā)的讀者都可以稍微了解一下 Tree Shaking 的概念喔。