原來專案打包也有這麼技巧 - 淺談 Tree Shaking 機制

前端小智發表於2022-06-08

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

前言

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

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

什麼是 Tree Shaking?

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

Dynamic Language & Static Language

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

接下來講個跟 Tree Shaking 比較無關的小知識,但這個小觀念可以幫助我們瞭解為何要在 JavaScript 上執行 Tree Shaking 並在程式語言中有分為 動態語言(Dynamic Language) 以及 靜態語言(Static Language),被歸類在 Dynamic Language 中比較常見的有 JavaScript、PHP、Python 等語不是我們想像中的那麼容易。

在程式語言中有分為 動態語言(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 原理之前必須要了解一個技術:死碼刪除(Dea誒 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 的誕生,我們可以在每個檔案的最上方先引用即將會需要用到的東西,所以這些 bunbler 就可以藉由這些 import file 很快速的知道可以保留哪些檔案,進而達到 Tree Shaking 的效果。

這時候讀者可能會有另一個問題了,在 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

image.png

named export

image.png

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

default export 經由 Tree Shaking 後的結果

image.png

named export 經由 Tree Shaking 後的結果

image.png

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

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

改善第三方元件的 import 方式

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

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

image.png

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

image.png

可以發現 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 差非常多,寫法如下。

image.png

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

image.png

可以發現整個 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。

image.png

Webpack 中的 usedExports

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

在官方檔案中有這麼一段說明:

image.png

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

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

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

image.png

小結

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

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


程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

作者:Andy Chen 譯者:前端小智 來源:medium

原文:https://medium.com/starbugs/%...

交流

文章每週持續更新,可以微信搜尋「 大遷世界 」第一時間閱讀和催更(比部落格早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了很多我的文件,歡迎Star和完善,大家面試可以參照考點複習,另外關注公眾號,後臺回覆福利,即可看到福利,你懂的。

相關文章