Photo by James Harrison on Unsplash.com

原來程式碼打包也有這麼多眉角 - 淺談 Tree Shaking 機制

Andy Chen
Starbugs Weekly 星巴哥技術專欄
11 min readApr 18, 2022

--

前言

身為一位前端工程師或多或少都有聽過 Webpack 這套前端打包工具吧,為了讓最終打包的檔案不會過於龐大,Webpack 可是下了非常多的苦功,例如:利用 Code Splitting 產出一個又一個的 chunk 檔讓網頁不會一次載入一份很大包的 JS 檔。

然而今天的文章其實不是要講 Code Splitting,而是要講一個比較深入的原理:Tree Shaking

什麼是 Tree Shaking?

Tree Shaking 就字面上翻譯來看就是搖晃樹木,在 Webpack 的世界中我們通常都會設定一個 Entry Points 來告訴 webpack 要從哪個檔案開始往其他檔案進行打包,如果用 Tree 的概念來看就是一個主幹配上很多的樹枝。

所以 Tree Shaking 最主要的目的就是要把沒有在使用的樹枝移除掉,不會被打包進去形成不必要的資源浪費。

Shake Tree Animal Crossing Twigs GIF By snáthaid_mhór on tenor

Dynamic Language & Static Language

接下來講個跟 Tree Shaking 比較無關的小知識,但這個小觀念可以幫助我們了解為何要在 JavaScript 上執行 Tree Shaking 並不是我們想像中的那麼容易。

在程式語言中有分為 動態語言(Dynamic Language) 以及 靜態語言(Static Language),被歸類在 Dynamic Language 中比較常見的有 JavaScript、PHP、Python 等語言,至於被歸類在 Static Language 比較常見的有 C++、Java 等語言。

在 Dynamic Language 中由於我們可以動態的載入非常多東西,例如 function、object 等,對於 Tree Shaking 來說這種會動態載入的物件實在是太難捉摸了,這也讓 Dynamic Language 的 Tree Shaking 很難達到最完美。

Dead Code Elimination

在開始講 Tree Shaking 原理之前必須要了解一個技術:死碼刪除(Dead Code Elimination)

在 compiler 的領域中,為了達到執行時間的優化,在程式碼編譯的過程中 compiler 會將對於最終結果沒有影響到的程式碼刪除,進而達到執行時間的優化,這段過程稱之為 Dead Code Elimination

乍看之下 Dead Code Elimination 在做的事情好像就是 Tree Shaking 要做到的事情,就是要刪除無用的程式碼,但兩者其實還是有著些微的差距,接下來就要講講 Tree Shaking 的原理。

Tree Shaking 原理

Tree Shaking 其實是 Dead Code Elimination 的一種新的實現原理,在上面的 Dynamic Language 的觀念中提到 Dynamic Language 的特性就是可以動態載入任何東西,因為這個特性讓 Dead Code Elimination 相當難實現,因為 complier 永遠不知道到底哪些程式碼是對最終結果不會有影響的。

所以 Tree Shaking 其實要做到的不會像 Dead Code Elimination 那樣死板板的要刪除對結果不會有影響的程式碼,而是要保留會需要用到的程式碼,這樣也可以達到類似 Dead Code Elimination 的效果,只是兩者的原理還是有一些差距,而這就是 Tree Shaking 的原理。

ES6 module v.s commonJS

上面提到 Tree Shaking 的原理最主要的目的就是要保留會需要用到的程式碼,而這點在早期的 JavaScript 其實是無法實現的,但是在 ES6 誕生後有一個非常重要的概念叫:ES6 modules。

由於 ES6 modules 的誕生,我們可以在每個檔案的最上方先引用即將會需要用到的東西,所以這些 bundler 就可以藉由這些 import file 很快速的知道可以保留哪些檔案,進而達到 Tree Shaking 的效果。

假如讀者對於 ES6 modules 不太熟悉的可以參考筆者之前寫過的文章

這時候讀者可能會有另一個問題了,在 ES6 module 還沒誕生以前我們也可以利用 commonJS 來進行 module 的載入,為什麼 ES6 module 可以做到 Tree Shaking 可是 commonJS 無法呢?

其實是因為 ES6 module 有著非常多的特性,讓 bundler 可以針對這些特性來進行靜態的分析像是:

  • module 必須要在頂層被 import。
  • module 內部會自動被定義為 strict mode
  • module name 不能動態改變。
  • module 內容為 immutable 無法在其他檔案中被動態新增或刪除內容。

因為這些強限制在,所以 ES6 module 就可以讓 bundler 做到 Tree Shaking 的效果,而 commonJS 則無法達到此點。

改善 import 與 export 方式

我們都知道 ES6 modules 的 export 方式有分 named export 以及 default export,這兩種方法適用於不同的使用情境,也會對 Tree Shaking 後的檔案內容有著非常大的差別。

左圖為 default export,右圖則為 named export

乍看之下 default exportnamed export 在寫法上好像沒什麼太大的差別(除了直接在項目前面加上 export 的寫法比較不一樣外),最終都是需要用一個物件來包裝輸出,但兩者在 Tree Shaking 後的結果可是有著蠻大的差別,接下來就看來一下 Tree Shaking 過後的結果吧!

左圖為 default export 經由 Tree Shaking 後的結果,右圖則為 named export 經由 Tree Shaking 後的結果

可以看到上面兩張圖,雖然 Tree Shaking 都有把 multiply 這個 function 移除了,可是 default export 相較於 named export 還是新增了不少變數來處理 function parameter,這樣就不是一個完美的效能優化。

所以假如讀者在開發時確定一個檔案會需要同時輸出很多項目,不管是物件也好函式也罷,這時候都建議用 named export 的方式進行輸出這樣才能達到最好的效能優化。

改善第三方套件的 import 方式

最後再來看一下 import 第三方套件的最佳方式,在前端開發的過程中為了不要重複照輪子很多時候都會使用大神所開發好的第三方套件來加速開發,但第三方套件的 import 方式其實也會影響到最終的 bundle size。

接下來筆者將以 ant design 這套 UI library 來進行說明。

首先是利用官方文件的說明來進行 import,其實 antd 本身就有針對其 module 進行 Tree Shaking 的效能優化,所以我們原則上是可以放心的使用官方文件的教學進行 import 的,接下來我們利用 webpack-bundle-analyzer 來進行檔案分析。

可以發現 antd 的檔案大小高達 842.15KB,而且裡面還跑出了許多跟 Button 無關的 component 檔案,這顯然是一個不好的 import 方式,沒想到照著官方文件的方式進行 import 也沒辦法達到最好的效能優化。

但這其實也不是 antd 的錯, antd 本身就有做好 Tree Shaking 的動作,詳細的說明可以參考 antd 的官方文件,但是筆者這邊的範例故意沒有在專案的 bundler 設定檔中開啟 Tree Shaking 的功能,進而導致 antd 的 Tree Shaking 失效。

雖然 bundler 沒有開啟 Tree Shaking 功能讓整體的 bundle size 過大,但我們其實也可以自己手動做這件事,這時候只要我們改成從 antd 的 es folder 進行元件的單獨 import 就可以讓最終的 bundle size 差非常多,寫法如下。

接著我們一樣使用 webpack-bundle-analyzer 來進行檔案分析。

可以發現整個 antd 的檔案大小少了非常多,只剩下 74.8KB 而且與 Button 無關的其他 component 都沒出現了,所以同一種第三方套件不同的 import 方式真的會讓整體的效能差距非常大,這個就是比較好的第三方套件 import 方式。

package.json 中的 sideEffects

在 Webpack 的 Tree Shaking 設定中,有一個可以在 package.json 中設定的叫 sideEffects,這個 sideEffects 的設定主要是讓 Webpack 這種 bundler 知道此專案是否可以做 Tree Shaking 的動作。

假如設定為 false 就代表可以將所有的檔案進行 Tree Shaking,若讀者知道有哪些檔案是不能做 Tree Shaking 的,這時候只要在 sideEffects 內用一個陣列將不能做 Tree Shaking 的檔案路徑寫上去,這時候 bundler 就只會針對這個陣列以外的檔案進行 Tree Shaking

package.json 中 sideEffects 寫法

Webpack 中的 usedExports

在 Webpack 的官方文件中要達到 Tree Shaking 的效果除了在 package.json 中加上 sideEffects 外,還可以在 Webpack 的設定檔中加上 usedExports

在官方文件中有這麼一段說明:

sideEffects v.s usedExports

如果說 sideEffects 在做的事情是把不能做 Tree Shaking 的樹枝移除,那 usedExports 在做的事情就是把樹枝上沒有用到的樹葉移除,所以 usedExports 其實才是在做真正的 Tree Shaking

useExports 利用 terser 這套工具進行檔案的 side effects 偵測,假如打包過程中發現此檔案既沒有 side effects 且某些程式碼又沒有被引用到,則該程式碼就會在之後的 uglify 被移除,藉此達到真正的 Tree Shaking 效果。

usedExports 的設定方式也非常簡單,只要在 Webpack 的設定檔中,在 optimization 內加上 usedExports: true 這時候就可以將 usedExports 的功能打開,寫法如下:

webpack config 中開啟 usedExports 寫法

小結

今天介紹了 Tree Shaking 的相關基本觀念,雖然說身為一位前端工程師不一定要懂這個觀念,畢竟現在很多主流的框架都已經先把 bundler 的相關 config 都寫好了,但了解這些工具背後在做的事情也能幫助到自己在開發時可以稍微省思一下要如何改良自己的程式碼,進而提升整體的打包後的效能。

像是上面提到的 import 與 export 方式,引用第三方套件時可以如何引用達到最小的 bundle size,有了這些觀念在開發時就可以提升整體的效能,所以筆者也建議目前正在學習網頁開發的讀者都可以稍微了解一下 Tree Shaking 的觀念喔~

Reference

--

--

Andy Chen
Starbugs Weekly 星巴哥技術專欄

嗨嗨我是Andy,用嘴巴工作的工程師😂,喜歡學習不同領域的內容,專長為網頁開發,歡迎大家跟我聊技術~