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工作的前提。

你可能感兴趣的:(webpack)