webpack二刷之五、生產環境優化(2.Optimization & Tree Shaking)

Optimization webpack 內部優化配置

webpack配置文件中的 optimization 屬性,用於集中去配置webapck內部的一些優化功能。

Tree Shaking 搖樹

字面意思就是伴隨着搖樹的動作,樹上的枯樹枝和樹葉就會掉落下來。

web開發術語 Tree Shaking 也是相同的道理,它表示「搖掉」代碼中未引用的部分(未引用代碼dead-code)。

MDN:Tree shaking 通常用於描述移除Javascript上下文中的爲引用代碼(dead-code)的行爲。

它依賴於ES6中的import和export語句,用來檢測代碼模塊是否被導出、導入,且被Javascript文件使用。

在webpack中就是將多個JS文件打包爲單個文件時,自動刪除未引用的代碼。以使最終文件具有簡潔的結構和最小化大小。

webpack生產模式中,自動開啓了Tree Shaking這個功能。

示例:

// /src/component.js
export const Button = () => {
  return document.createElement('button')

  console.log('dead-code')
}

export const Link = () => {
  return document.createElement('a')
}

export const Heading = level => {
  return document.createElement('h' + level)
}

// /src/index.js
// 只導入一個成員
import { Button } from './component'

document.body.append(Button())

使用生產模式打包後,Tree Shaking的效果就是,只將Button打包進輸出文件,其他兩個成員由於未使用,而沒有打包到輸出文件。

// /dist/main.js
!(function (e) {
  /*...*/
})([
  function (e, t, n) {
    'use strict'
    n.r(t)
    // 只有Button
    document.body.append(document.createElement('button'))
  },
])

手動開啓 Tree Shaking

Tree Shaking並不是 webpack 的某個配置選項。

它是一組功能搭配使用後的優化效果。

這組功能會在生產模式production下自動使用。

webpack官方文檔對 Tree Shaking 介紹有些混亂,這裏學習如何手動開啓它,學習它的工作過程和優化功能。

上例代碼,使用none模式打包,查看打包文件,components.js模塊依然保留了LinkHeading

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
  
// 導出了3個成員
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Button", function() { return Button; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Link", function() { return Link; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Heading", function() { return Heading; });

// components.js的內容全部打包進來了
const Button = () => {
  return document.createElement('button')

  console.log('dead-code')
}

const Link = () => {
  return document.createElement('a')
}

const Heading = level => {
  return document.createElement('h' + level)
}

})

通過配置optimization中的usedExports和minimize優化功能實現Tree Shaking。

usedExports

usedExports: true 表示在輸出結果中模塊只導出外部使用了的成員。

打包查看輸出文件:

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// 只導出了Button
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
  
// Link和Heading被標記未使用
/* unused harmony export Link */
/* unused harmony export Heading */

// components.js的內容全部打包進來了
const Button = () => {
  return document.createElement('button')

  console.log('dead-code')
}

/** 沒有用到的代碼 start **/
const Link = () => {
  return document.createElement('a')
}

const Heading = level => {
  return document.createElement('h' + level)
}
/** 沒有用到的代碼 end **/
})

此時就可以通過壓縮優化,刪除掉「沒有用到的代碼」。

minimize

minimize: true開啓代碼壓縮優化。

刪除註釋、刪除沒有用到的代碼、刪除空白、替換變量名爲簡短的名稱等。

它使用的是TerserPlugin或optimization .minimizer中指定的插件。

再次打包:

function (e, t, n) {
    'use strict'

    /*
    壓縮前:
    __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
    */
    n.d(t, 'a', function () { return r })


    /*
    壓縮前:
    const Button = () => {
      return document.createElement('button')
    
      console.log('dead-code')
    }
    */
    const r = () => document.createElement('button')
  }
總結

webpack打包後,將每個模塊放到一個函數中,其中包含對成員的定義 和 對成員的導出。

usedExports 可以標記模塊導出的成員是否被外部使用,從而在打包結果中,不導出未使用的成員。

標記打包後表現爲:包裹模塊的函數中保留定義這些成員的代碼,但是移除導出它們的代碼,並添加註釋/* unused harmony export */

而函數中沒有了導出它們的代碼,也就表示這些成員未使用,那定義它們的代碼也沒有了意義,minimize就會將這些未使用的定義成員的垃圾代碼一併刪除。

  • usedExports 負責標記「枯樹葉、枯樹枝」
  • minimize 負責「搖掉」它們

concatenateModules 合併模塊

普通的打包結果,是將每個模塊單獨放在一個函數中。

如果模塊很多,打包結果中就會有很多這樣存放模塊的函數。

開啓concatenateModules: true打包後,打包後的文件中,就不是一個模塊對應一個函數,而是將所有模塊都放在一個函數中。

concatenateModules的作用就是儘可能的將所有模塊合併輸出到一個函數中。

既提升了運行效率,又減少了代碼的體積。

這個特性又被稱爲「Scope Hoisting」,也就是作用域提升。

它是webpack3增加的特性。

Tree Shaking & Babel

由於webpack早期發展非常快,變化比較多。

當我們找資料時,找到的結果,並不一定適用於當前所使用的版本。

比如 Tree Shaking,很多資料中都表示 如果 使用了 babel-loader,就會導致 Tree Shaking 失效

在這裏統一說明一下。


首先要了解,Tree Shaking 實現的前提,是基於 「必須用 ES Modules 組織代碼」。

即交給webpack處理的代碼,必須使用 ESM 方式實現模塊化。

MDN:Tree shaking 依賴於ES6中的import和export語句,用來檢測代碼模塊是否被導出、導入,且被Javascript文件使用。

webpack優化的過程是先將代碼交給loader去處理,然後再將處理結果優化輸出。

而爲了轉換代碼中的 ECMAScript 新特性,一般會選擇babel-loader去處理JS。

而在babel-loader處理代碼時,就有可能將代碼中的 ESM 轉換爲 CommonJS。

PS:實際上這取決於是否使用了轉換ESM的babel插件,而常用的插件集合 @babel/preset-env 就包含轉換ESM的插件。

所以當 @babel/preset-env 工作時,代碼中的 ESM 就應該被轉換爲 CommonJS。

所以webpack在打包時,拿到的就是 CommonJS 組織的代碼,從而 Tree Shaking 也就不能生效。


但是現在的效果並不是這樣。

在項目中使用babel-loader,並僅開啓usedExports,查看打包結果。

module.exports = {
  mode: 'none',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  optimization: {
    usedExports: true,
  },
}

(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return Button; });
  
// usedExports 生效了,也就是Tree Shaking 生效了
  
/* unused harmony export Link */
/* unused harmony export Heading */
  
var Button = function Button() {
  return document.createElement('button');
  console.log('dead-code');
};
var Link = function Link() {
  return document.createElement('a');
};
var Heading = function Heading(level) {
  return document.createElement('h' + level);
};

})

發現 usedExports 生效了,也就表示 Tree Shaking 並沒有失效。

這是因爲在最新版本的 babel-loader 中自動關閉了 轉換ESM 的插件。

可以查看 node_modules/babel-loader 模塊的源代碼(/lib/injectCaller.js)。

其中已經通過supportsStaticESM supportsDynamicImport標識了支持 ESM 和 動態import 方法(Webpack >= 2 supports ESM and dynamic import.)。

"use strict";

const babel = require("@babel/core");

module.exports = function injectCaller(opts, target) {
  if (!supportsCallerOption()) return opts;
  return Object.assign({}, opts, {
    caller: Object.assign({
      name: "babel-loader",
      // Provide plugins with insight into webpack target.
      // https://github.com/babel/babel-loader/issues/787
      target,
      // Webpack >= 2 supports ESM and dynamic import.
      supportsStaticESM: true,
      supportsDynamicImport: true,
      // Webpack 5 supports TLA behind a flag. We enable it by default
      // for Babel, and then webpack will throw an error if the experimental
      // flag isn't enabled.
      supportsTopLevelAwait: true
    }, opts.caller)
  });
};
// ...

再去查看插件集合源代碼(node_module/@babel/preset-env/lib/index.js)。

翻到下面的代碼:

const modulesPluginNames = getModulesPluginNames({
    modules,
    transformations: _moduleTransformations.default,
    shouldTransformESM: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsStaticESM)),
    shouldTransformDynamicImport: modules !== "auto" || !(api.caller == null ? void 0 : api.caller(supportsDynamicImport)),
    shouldParseTopLevelAwait: !api.caller || api.caller(supportsTopLevelAwait)
  });

看到preset-env根據babel-loader的標識,自動禁用了 ESM 和 動態import 的轉換。

所以webpack通過babel-loader轉換後,打包時還是ESM組織的代碼,Tree Shaking也就能正常工作。


可以通過修改插件集合的配置,開啓 ESM 轉換:

module: {
  rules: [
    {
      test: /\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          // 注意:僅使用時,是將插件集合名稱放到一個數組中
          // presets: ['@babel/preset-env'],

          // 當對插件集合編寫配置時,就需要再套一個數組
          // 數組的第一個元素是插件集合的名稱
          // 第二個元素是它的配置對象
          presets: [
            [
              '@babel/preset-env',
              {
                // modules默認是auto,即根據環境去判斷是否開啓轉換ESM插件
                // 這裏設置爲強制轉換爲commonjs
                modules: 'commonjs',
              },
            ],
          ],
        },
      },
    },
  ],
},

再次打包查看:

(function(module, exports, __webpack_require__) {

"use strict";


Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Heading = exports.Link = exports.Button = void 0;

var Button = function Button() {
  return document.createElement('button');
  console.log('dead-code');
};

exports.Button = Button;

var Link = function Link() {
  return document.createElement('a');
};

exports.Link = Link;

var Heading = function Heading(level) {
  return document.createElement('h' + level);
};

exports.Heading = Heading;

})

3個成員都被導出,從而壓縮優化時,也不會將它們刪除。

總結

通過以上實驗發現,最新版本的babel-loader,並不會導致Tree Shaking失效。

如果還不確定,也可以嘗試將preset-env配置中的modules,設置爲false

這樣就會確保,preset-env不會開啓 轉換ESM 的插件。

同時確保了Tree Shaking工作的前提。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章