avatar
instagramthreads

Tree-shaking 的 sideEffects、optimization 設定

tree-shaking banner

什麼是 side effect?

根據 Webpack 官方文件,這裡的 side effect 是指當我們在 import 某個模組時,這個模組裡除了明確被 export 出來的內容外,還額外會在 import 時去執行某些操作的代碼,就稱為 side effect。

舉個例子,在前面起始專案中的 array.js 內容是這樣:

// src/utils/array.js
console.log("[flattenDeep] declared here!");

const flattenDeep = (arr) =>
  arr.flatMap((subArray) =>
    Array.isArray(subArray) ? flattenDeep(subArray) : subArray
  );

export { flattenDeep };

當你在 import 這個模組時,雖然 array.js 裡沒有把 console.log 的內容去做 export,但這段程式碼仍然會被執行,這就是 side effect。

另一種 side effect 的經典例子是 polyfill,參考下面這個例子,因為 polyfill 通常不提供 export 而是直接去對整個 global scope 作用,因此也會有 side effect:

// polyfill.js
if (!Array.prototype.includes) {
  Array.prototype.includes = function(element) {
    // 實現 includes 方法的代碼
    return this.indexOf(element) !== -1;
  };
}

回到正題,Webpack 在打包時也能參考 package.json 中的 sideEffects 設定來判斷是否要移除確定沒有副作用的程式碼,以下來實驗看看。

package.json 中的 sideEffects 選項拿掉後,進行 production build:

(() => {
  "use strict";
  console.log("[flattenDeep] declared here!");
  console.log("[add] declared here!");
  const e = (e, l) => e + l;
  console.log("[multiply] declared here!");
  var l, o;
  console.log(e((l = 5), (o = 6)) / e(o, l));
})();

在 package.json 中加上 sideEffects 選項後,進行 production build

// pacakge.json
"sideEffects": false,
(() => {
  "use strict";
  console.log("[add] declared here!");
  const e = (e, l) => e + l;
  console.log("[multiply] declared here!");
  var l, o;
  console.log(e((l = 5), (o = 6)) / e(o, l));
})();

可以注意到差別在 array.js 中的 flattenDeep 前的 console.log 是否被移除的差別,也就可以確認 sideEffect 這個選項的用途與使用方式,當你想保留那些你有 import 的內容,但實際沒被 export 的,可以加在 sideEffects 的陣列中告訴 Webpack 這些路徑的 pattern 有副作用,不需要幫我移除掉。

另外比較常見的是 side effect 如 import ‘./style.scss; 這樣的樣式檔,為了避免在過程中被錯誤 shake 掉,也記得加入到 sideEffects array 中:

sideEffects: ["*.scss"]

Webpack 的 optimization 設定

接下來講幾個 Webpack 不好懂且容易搞混的 optimization 與 tree-shaking 有相關的選項。這些選項在 production mode 預設都會被啟用,所以其實不需要做什麼特別設定專案就會自動完成 tree-shaking 的配置了。

optimization.sideEffects

你可能會說,這個 sideEffects 跟 package.json 中的 sideEffect 有什麼差別,沒錯,他們有關但不是指同一件事。

這個 optimization.sideEffects 指的是「告訴 Webpack 是否要去看 package.json 中的 sideEffects 選項」,可能有點難懂。

一樣看個範例試試看這樣的配置,並去執行 production build:

// package.json
"sideEffects": ["src/utils/array.js"],

// webpack.config.js
optimization: {
  sideEffects: false,
},
(() => {
  "use strict";
  console.log("[flattenDeep] declared here!");
  console.log("[add] declared here!");
  const e = (e, l) => e + l;
  console.log("[multiply] declared here!");
  var l, o;
  console.log(e((l = 5), (o = 6)) / e(o, l));
})();

可以看到最後 side effect 的 console.log("[flattenDeep] declared here!") 沒有被移除,這是因為在 optimization.sideEffects 中你請 Webpack 在打包時不要去參考 package.json 中的 sideEffects 選項,因此得到這樣的結果。

總歸來說,這個選項是 Webpack 提供一個彈性讓你可以開關是否要去參考 package.json 中的 sideEffects 選項,但預設是開啟的,所以也不用特別調整。

optimization.providedExports

預設為 true 開啟,此選項被開啟時有幾種用途:

  • 自動識別導出:Webpack 嘗試分析每個模組,來確定其中有哪些內容被 export
  • 針對 export * from ... 這樣的語法做優化,避免非必要的 export
  • 減少 bundle 大小:在明確分析模組內哪些內容有被 export 後,可以移除無用的內容

optimization.sideEffects 選項會依賴於這個選項需要被開啟。

optimization.usedExports

官方文件中有一段在描述其實 sideEffectsusedExport 雖然都是為了要減少 bundle size,但其實不是同一件事:

  • sideEffects :用途是標記哪些模組、檔案路徑,雖然其中有內容沒有被 export,但這些內容需要能被保留下來,而沒有被標記的部分可以交由 bundler 放心移除。
  • usedExport :預設在 production mode 是 true。這個設定用來決定是否要在打包過程中利用 terser 去標記上前面提到的 unused harmony export,最後才靠 Minimizer (如老牌的 terser 或速度極快的 esbuild) 去實際將被標記的程式碼移除,達到真正的 tree-shaking 效果。

References

參考資料及延伸閱讀統一放在最後一篇中。