精準的打包 — Webpack 的 Tree Shaking

神Q超人
Starbugs Weekly 星巴哥技術專欄
9 min readJan 31, 2022
Photo by Bench Accounting on Unsplash

Hi!大家好,我是神 Q 超人!前陣子在和朋友聊 Webpack 的時候,突然提到 Tree Shaking,但很慚愧的是我沒有辦法好好說明 Webpack 是如何做到 Tree Shaking 的,因此就趁這個年假的第一天抽空讀 Webpack 的文件,然後把理解到的心得寫下來,如果你也有興趣,就一起看下去吧 🙌。

Tree Shaking 是什麼

Tree Shaking 是個優化的方式,在 JavaScript 中用來表示移除沒用的程式碼的一個常見術語,之所以叫做 Tree Shaking 的由來似乎是指說「當你大力搖晃一棵樹的時候,樹上就只會留著綠色的葉子,其他枯葉都會落到地上」,而那些綠色的葉子就是打包過後的文件中,真正有用到的程式碼。

在使用時要注意的是,Tree Shaking 只能夠使用在 static structure(例如:import 和 export 上),像是 dynamic structure 的 require 就沒辦法被偵測到。舉例來說,import 要載入某個 module 使用的話就一定要在文件的最上方,但 require 可以在任何地方使用,例如以下場景就必須要等到 runtime 才會知道 module 是什麼:

那開始了解 Tree Shaking 的運作前,應該會有些人好奇,就算自己從來就沒有特別在 Webpack 中設置 Tree Shaking,但是沒有用的程式碼也都會被移除呀!

那是因為 Tree Shaking 的執行需要 ModuleConcatenationPlugin(圖一),而 Webpack 裡另外有個 mode,如果你一直沒有特別去設置 mode 的值,那 mode 就預設會是 production(圖二),然後 production 的預設選項中就會開啟 ModuleConcatenationPlugin(也是圖二),因此平常不會特別注意到也不奇怪,因為 Webpack 都幫你做好了。

圖一(https://webpack.js.org/guides/tree-shaking/#minify-the-output
圖二(https://webpack.js.org/configuration/mode/#mode-production

Tree Shaking 的運作

因為 Production 會幫你打開 ModuleConcatenationPlugin 的關係,所以待會我們實驗的時候,要把 mode 改成 none(Webpack 文件說 none 為關掉所有優化設置的模式)。

這邊會附上簡單的 初始專案配置,有興趣的話,可以把它 clone 下來一起玩看看。

首先在 src 下建立一個 math.js 和 string.js,接著個別寫下一個方法做 export,分別是 add 和 composeString:

打開 src 下的 index.js,把 add 和 composeString 都 import,但只使用 add 方法:

最後到 terminal 中執行 npm run build 或是 webpack 做打包,打包結束後,會發現雖然我們只有 import add 做使用,但是打包後的檔案內容還是會有 composeString:

不過這很正常,畢竟我們還沒有做任何處理,Webpack 在打包時也不曉得你哪些程式碼到底有沒有用到,就沒辦法幫你把 composeString 移除。

那麼到底什麼樣的程式碼是有用的,怎樣是沒用的呢?

  1. 最明顯的定義應該是,如果有被執行就代表有用到。像是上方案例的 add 一樣。
  2. 有 side effect 的程式碼也是被用到的。像是上方的 index.js,看起來什麼方法都沒有提供,但是執行時卻會在 console 中留下 log,除此之外,會改變執行環境的 polyfill 也是有 side effect 的 library,。

第一種情況相對容易分辨,但如果是第二種情況的話,可以選擇用 Webpack 中的 sideEffects 屬性來設置。

sideEffects

sideEffects 可以被設置為 Boolean 或是 Array,當你把它設置為 false 的時候,代表該專案是不會有 sideEffects 的,也就是一律用 export 判斷是否使用。另外 sideEffects 會依賴 providedExports,用來找出專案中所有 export 的 module:

https://webpack.js.org/configuration/optimization/#optimizationsideeffects

以下是 sideEffects 的使用方式:

只要在 package.json 中加上 sideEffects,並且將值設定為 flase,就代表該專案內所有的程式碼都沒有 side effect,因此 Webpack 在打包的時候,就可以把沒有用到的 export 程式碼給移除。

加上 sideEffects 後打包,就不會看到 composeString 在結果裡了:

那現在我們再到 src 中建立另一個 polyfill.js,在 ployfill.js 裡為 Array 建立自定義的方法,再把它 import 到 index.js 中:

如果我們去打包上方的程式碼,polyfill.js 會因為沒有任何 export,所以不會被 providedExports 抓到,也就不會被打包到 Production,這會導致專案裡如果有使用到 Array 的 customMethod,在執行時就會出錯。面對這種情況,就必須要在 sideEffects 屬性中告知,polyfill.js 是有 side effect 的。設置方法如下:

如此一來,polyfill.js 就會直接被打包了:

最後要注意兩件事情:

  1. 如果各位的專案中也有 import .css 樣式檔來用的話,也記得要將 .css 結尾的檔案放到 sideEffects,例如 sideEffects: ["*.css"]
  2. 在 webpack.config.js 裡的 optimization 也有 sideEffects,但在這裡設置的值是針對 node_modules 中的。

useExported

useExported 的作用和 sideEffects 都是用來判斷是否該移除程式碼,但根據 Webpack 文件內的說明,useExported 才是真正的 Tree Shaking:

https://webpack.js.org/guides/tree-shaking/#clarifying-tree-shaking-and-sideeffects

usedExports 會使用 terser 判斷程式碼有沒有 side effect,如果沒有用到,又沒有 side effect 的話,就會在打包時替它標記上 unused harmony,並在 minify(用 Uglifyjs 或其他工具)的時候移除。

在測試 usedExports 之前,先到 math.js 裡加入 square 並 export:

接下來到 webpack.config.js 中加入 optimization.usedExports:

然後對專案進行打包,就會發現僅僅是 export,但沒有使用的 square 會被標記上 unused harmony export:

接著我們使用 uglifyjs-webpack-plugin,把沒有用到的 square 從樹上搖晃下來:

npm install -d uglifyjs-webpack-plugin

webpack.config.js 的設置如下:

設置完 minimizer 後,再打包一次,就能看見 square 已經被移除了:

usedExports 與 sideEffects 不同的是,usedExports 可以以陳述句為單位去判斷是否有 side effect,但是 sideEffects 可以讓 Webpack 在打包的時候,直接略過一整個檔案,只要是出現在 sideEffect 裡的檔案就是直接打包,也不用透過 terser 評估副作用。

總結

  1. Tree Shaking 只能在 static structure 使用,如果專案中的 babel 會將 static structure 編譯成 dynamic structure 的話,要另外設置(可以看 這篇文章
  2. 使用 sideEffects 時,要寫在 package.json,如果是要對第三方函式庫優化,要寫在 webpack.config.js 裡的 optimization。
  3. usedExports 才是 Tree Shacking,使用時會自動判斷沒使用的程式碼,並標記 unused harmony 的註解,要移除的話要另外使用 minify。

本來以為 Tree Shaking 小小一篇而已,應該不會讀得太久,但是沒有想到其中順著文件裡提供的連結,東看看,西看看,一整天就這樣耗掉了 😂。雖然平常都是直接使用 Webpack 的 Production 替我們做預設的優化配置,但這次自己親自瞭解一些關於前端的效能優化後,感覺也滿不錯的。

最後如果文章中有任何問題、或是理解錯誤的部分,再麻煩留言告訴我,我會盡快回覆及修正的,非常感謝 🙇‍♂️

參考文章

--

--