什麼是 tree-shaking?
Table of Contents
前言
這陣子剛好工作上碰到相關議題,興趣使然也想深入了解一下,確保專案在 tree-shaking 的各種眉角上設定完,是能正確有效的增進效能,因此把筆記整理出來讓未來有同樣需求的人有跡可循。
這篇文章將由淺入深地探討 tree-shaking,從基本的 Why、What、How 介紹,並透過實例 demo 來理解這些不太好懂的設定們。
另外本文會用 Webpack、Babel 設定來講解原理與注意事項,因為當你經手的是以前使用 create-react-app 或 Vue CLI 或更近代的 create-next-app 來建立 repo 的專案,預設都仍會使用 Webpack。
而關於 bundler 相關生態圈 (包含 transpiler、minifier 等) 的愛恨糾葛效能大比拼,如 Vite/Rollup、rspack、Turbopack、SWC、esbuild、Bun.build 等,因為不完全是 tree-shaking 相關,可能之後有機會再用另一篇文章來說明。
什麼是 tree-shaking?
參考 Webpack 官方文件形容,你可以想像你整個專案的程式碼是一棵依賴樹,而那些你實際有用到的程式碼與套件就像樹上綠色的葉子,沒用到的部分則是枯黃的葉子,為了要在最後打包程式碼時,將整個專案中沒用到的部分被去除掉,你需要去搖動這棵樹。
Tree-shaking 最早的起源來自 Rollup 的其中一個特性,是為了在 JavaScript 模組的打包過程中,讓最後的輸出檔案中,只包含實際用到的程式內容。
此概念其實與編譯器原理中的死碼刪除 (Dead Code Elimination, DCE) 有些類似,因此當年社群有人抱怨為何 Rollup 還要重新定義 DCE。
Rollup 作者 Rich Harris 在 2015 的文章中提到,儘管 tree-shaking 與 DCE 都是為了得到「更少的程式碼」,但其實是不一樣的概念。
JavaScript 的限制
在理解為什麼 tree-shaking 與 DCE 不是同一件事前,要先知道一個 JavaScript 的特性。程式語言分為動態語言、靜態語言,而 JavaScript 屬於動態語言。
動態語言最大的優點是語法簡潔、有較高的彈性,諸如變數型別、物件屬性等都可以在程式執行時被改變。但這也是一把雙面刃,缺點就是容易遇到 runtime error 問題,以及較無法被靜態分析。
而這個靜態分析,也就成了 JavaSciprt 打包工具的一個難題:「既然我無法確切知道這些程式在執行後會不會被用上,我又要怎麼放心的刪除它呢?」
Tree-shaking vs DCE
想像你今天要做蛋糕,DCE 的做法是「把所有雞蛋都丟進攪拌碗裡一起搗碎後,拿去烤箱烤完再去除蛋殼碎片。」
而 tree-shaking 的思維是「如果我今天想做蛋糕,我需要在攪拌碗中加入哪些成分?」
從上述例子中,可以知道 DCE 的做法是將成品中不需要的內容刪掉,而 tree-shaking 指的是從一開始就只加入需要的部分。
前面提到 JavaScript 不太好被靜態分析的限制,因此 DCE 這個策略是不可行的,也因此才需要 tree-shaking 的這種做法:「從源頭判斷哪些模組沒被用到可以被移除」。
要怎麼做到 tree-shaking?
Tree-shaking 的原理是透過靜態分析的方式,在打包前從程式碼進入點開始去分析整顆依賴樹,每個模組是否有被使用到需要打包進來。
這件事在早期仍在使用 CommonJS (CJS)、AMD、UMD 等程式中是做不到的,因為這些模組化寫法都允許自由奔放的動態載入。
一直到後來 ES2015 module (ESM) 出現後,提供了靜態的 import / export 寫法,讓打包工具可以在編譯時就明確知道導入、導出內容,才有辦法做到 tree-shaking。
Tree-shaking 的實例 Demo
Talk is cheap,我們先來直接看個例子體驗一下什麼是 tree-shaking。
假設我們在 utils.js
中宣告了 2 個 function,add
與 multiply
:
另外在 index.js
中去引入其中的 add
來使用:
讓我們看看執行 Webpack 打包後的結果,為了方便觀察行為,我們先使用 mode: development
打包,以下為其中一段較關鍵的輸出:
可以注意到這裡有兩個關鍵字:
- /* harmony export */
- /* unused harmony export */
Webpack 的 module parser,在打包過程會有以下步驟:
- 靜態分析:對整個 ESM 依賴樹的 import、export 去分析哪些被 export 的部分並沒有被使用。
- 辨識標記:將沒被使用的 ESM 做
unused harmony export
標記,但還不會實際去刪除,而是會留到後續交給 Minifier 去刪除。
接著如果再試著執行 mode: production
打包後的全部輸出,為方便解析套了 formatter 做排版:
可以看到 multiply
函式因為沒被使用而有成功被 tree-shake 掉。
實現 tree-shaking 需要注意哪些事
實際看完 tree-shaking 的效果後,接著就要來了解如果要做到 tree-shaking 需要注意哪些眉角。
根據 Webpack 官方文件結論及實驗結果,要做到 tree-shaking 的話有以下幾點是必備的:
- 使用 ESM 語法
- 確保 transpiler 如 Babel 的設定中,沒有把你專案中的 ESM 轉成 CommonJS (CJS)、AMD、UMD 模組
- 使用
mode: production
啟動預設各種優化 plugins 與 optimization 選項,包含 minification 與 tree-shaking。
也因為 tree-shaking 在進行 production build 時預設就有開啟,也就是說當你前兩點都滿足的話其實不用特別設定什麼。
另外也有一些可以自定義的選項以及注意事項:
- 將
sideEffects
選項加到專案的package.json
中 - 注意 Webpack tree-shaking 不支援 dynamic import(參考官方 issue)
- 確認 Webpack config 中的 devtool 設定是否正確設定,有些模式在 production mode 下不適用
- devtool 是指定產生 source map 的方式
- 通常在 production mode 會用 SourceMapDevToolPlugin 來做更多進階設定
- import / export 方式對 tree-shaking 的影 響
References
參考資料及延伸閱讀統一放在最後一篇中。