精準的打包 — Webpack 的 Tree Shaking

前端小智發表於2022-02-17
作者: 神Q超人
譯者:前端小智
來源:medium

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

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

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

Tree Shaking 是什麼

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

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

let module = null;

if (Math.random() * 10 > 5) {
  module = require('module1');
} else {
  module = require('moudle2');
}

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

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

image.png

image.png

Tree Shaking 的運作

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

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

首先在 src 下建立一個 math.jsstring.js,接著個別寫下一個方法做 export,分別是 addcomposeString

const add = (a, b) => a + b;

export default { add };

const composeString = (a, b) => `${a} ${b}`;

export default { composeString };

開啟 src 下的 index.js,把 addcomposeStringimport,但只使用 add 方法:

import { add } from './math';
import { addString } from './string';

console.log(add(1, 2))

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

image.png

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

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

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

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

sideEffects

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

image.png

以下是 sideEffects 的使用方式:

{
  "name": "tree-shaking",
  "sideEffects": false,
  "version": "1.0.0",
  ...
}

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

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

image.png

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

index.js

import './polyfill';
import { add } from './math';
import { addString } from './string';

console.log([].customMethod());

polyfill.js

Array.prototype.customMethod = () => {
  console.log('customMethods');
};

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

{
  "name": "tree-shaking",
  "sideEffects": ["./src/polyfill.js"],
  "version": "1.0.0",
  ...
}

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

image.png

最後要注意兩件事情:

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

useExported

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

image.png

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

在測試 usedExports 之前,先到 math.js 裡加入 squareexport

const add = (a, b) => a + b;

const square = (a, b) => a * b;

export { add, square };

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

module.exports = {
  ...
  optimization: {
    usedExports: true,
  }
};

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

image.png

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

npm install -d uglifyjs-webpack-plugin

webpack.config.js 的設定如下:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  ...
  optimization: {
    usedExports: true,
    minimize: true,
    minimizer: [
        new UglifyJsPlugin({
            uglifyOptions: {
                compress: { unused: true },
                mangle: false,
                output: {
                    beautify: true
                }
            },
        })
    ],
  }
};

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

image.png

usedExportssideEffects 不同的是,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

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

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

交流

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

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

相關文章