為什麼 Babel 設定會影響 tree-shaking?

為什麼 Babel 設定會影響 tree-shaking?
這裡我們一樣看個實際例子,完整的初始 template 可以參考這個 repo,這次我們宣告出了 3 個 function,以及實際上使用的只有 add
:
// src/utils/array.js
console.log("[flattenDeep] declared here!");
const flattenDeep = (arr) =>
arr.flatMap((subArray) =>
Array.isArray(subArray) ? flattenDeep(subArray) : subArray
);
export { flattenDeep };
// src/utils/math.js
console.log("[add] declared here!");
const add = (a, b) => {
return a + b;
};
console.log("[multiply] declared here!");
const multiply = (a, b) => {
return a * b;
};
export { add, multiply };
// src/utils/index.js
export { flattenDeep } from "./array";
export { add, multiply } from "./math";
// src/index.js
import { add } from "./utils";
const init = (a, b) => {
return add(a, b) / add(b, a);
};
console.log(init(5, 6));
再看到其中的 webpack.config.js
中,這裡可以注意到其中在 babel-loader
中有個 @babel/preset-env
的設定:
// webpack.config.js
rules: [
{
test: /\.(?:js)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
modules: "auto",
},
],
],
},
},
},
],
大多數專案中會套用 @babel/preset-env 這個 Babel 的 preset (預設配置),或是更古老的專案中可能會看到類似 babel-preset-2015
之類的 preset,以下也先岔個題簡單整理一下這些設定的意思。
關於 @babel/preset-env
你可能知道為了要能讓 ES6+ 的各種 Modern JavaScript 語法能在不同瀏覽器與版本都能兼容,我們需要用 Babel 來做轉譯與套用對應的 polyfill,而早期需要自己一個一個手動設定,像是這樣的範例:
// babel.config.js
module.exports = {
presets: [
'babel-preset-es2015', // 轉換 ES6 語法
'babel-preset-es2016', // 轉換 ES7 語法
],
plugins: [
'@babel/plugin-transform-nullish-coalescing-operator', // ??
'@babel/plugin-transform-optional-chaining', // ?.
...
]
};
這樣設定的問題是如果當未來某個瀏覽器停止支援、某個版本後開始支援某個語法、產品決定停止支援某版本以前使用者等,都可能要回頭手動調整 Babel 設定不夠智慧,且若沒有回頭調整也會因為打包了許多其實不需要的 polyfill 而造成 bundle 肥大,因此 @babel/preset-env
就派上用場了。
所謂的 preset 是指預設配置,這個 @babel/preset-env
可以根據你在專案中設定的 .browserlistrc
來智慧地去 mapping 在轉譯時要引入哪些 core-js polyfill,讓你打包的結果可以支援你想支援的各種瀏覽器以及其對應的版本。
@babel/preset-env 中的 modules
設定
回到正題,那為什麼 Babel 設定會與 tree-shaking 有關呢?這跟其中的 modules
這個設定有關,這個設定的意思是「當 babel-loader 在看到 ESM 時要轉譯成什麼模組」:
false
:不要轉譯,保留原本 ESM’auto’
:預設選項,意思是依照 babel-loader 中的supportsStaticESM
選項去決定行為:true
: 等同於modules: false
的效果false
: 等同於modules: "commonjs"
的效果
- 其他選項:將 ESM 轉譯成對應的 CJS、AMD、UMD 等模組
而其中的這個 'auto'
在 Webpack >= 2 中,supportsStaticESM
預設為 true
。也就是說預設如果將 modules
設為 ’auto’
與 false
的效果相同,都是告訴 Babel 在看到 ESM 時不要做轉譯。
範例
上面講的可能有點複雜,一樣直接來看個範例實際體驗一下,當我們把 modules 選項設定為 ’auto’
後,去做 production build,也就是在範例專案中執行 npm run build:prod
:
// webpack.config.js
presets: [
[
"@babel/preset-env",
{
modules: "auto",
},
],
],
// dist/bundle.js
(() => {
"use strict";
console.log("[add] declared here!");
const e = (e, l) => e + l;
console.log("[multiply] declared here!"), console.log(e(5, 6) / e(6, 5));
})();
可以看到 tree-shaking 有生效,沒被使用到的 function 都被 shake 掉了,只有 add
被保留下來 。
當我們把 modules 選項設定為 ’cjs’
後,也就是跟 Babel 說「當你看到 ESM 時,請幫我轉譯成 CommonJS 模組」,此時若去做 production build:
// webpack.config.js
presets: [
[
"@babel/preset-env",
{
modules: "cjs",
},
],
],
// dist/bundle.js
(() => {
"use strict";
var e,
t = {
980: (e, t) => {
Object.defineProperty(t, "__esModule", { value: !0 }),
(t.flattenDeep = void 0),
console.log("[flattenDeep] declared here!");
const r = (e) => e.flatMap((e) => (Array.isArray(e) ? r(e) : e));
t.flattenDeep = r;
},
438: (e, t, r) => {
Object.defineProperty(t, "__esModule", { value: !0 }),
Object.defineProperty(t, "add", {
enumerable: !0,
get: function () {
return o.add;
},
}),
Object.defineProperty(t, "flattenDeep", {
enumerable: !0,
get: function () {
return l.flattenDeep;
},
}),
Object.defineProperty(t, "multiply", {
enumerable: !0,
get: function () {
return o.multiply;
},
});
var l = r(980),
o = r(763);
},
763: (e, t) => {
Object.defineProperty(t, "__esModule", { value: !0 }),
(t.multiply = t.add = void 0),
console.log("[add] declared here!"),
(t.add = (e, t) => e + t),
console.log("[multiply] declared here!"),
(t.multiply = (e, t) => e * t);
},
},
r = {};
(e = (function e(l) {
var o = r[l];
if (void 0 !== o) return o.exports;
var d = (r[l] = { exports: {} });
return t[l](d, d.exports, e), d.exports;
})(438)),
console.log((5, 6, (0, e.add)(5, 6) / (0, e.add)(6, 5)));
})();
上面可以看到可怕的事情發生了,那就是 tree-shaking 並沒有生效,雖然只有用到 add
,但 flattenDeep
與 multiply
也都被留下來了。這是因為 tree-shaking 的必備條件是只能靜態分析 ESM 模組,因此千萬要注意 babel-loader
中的 @babel/preset-env
設定是否正確。
References
參考資料及延伸閱讀統一放在最後一篇中。