avatar
instagramthreads

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

tree-shaking banner

為什麼 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,但 flattenDeepmultiply 也都被留下來了。這是因為 tree-shaking 的必備條件是只能靜態分析 ESM 模組,因此千萬要注意 babel-loader 中的 @babel/preset-env 設定是否正確。

References

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