Webpack Tree Shaking 使用小结

说起 Tree Shaking,相信大家都不陌生,它主要用于清除符合 ESModule 规范、在程序上下文环境中未被引用的模块导出代码。在 Webpack 中,主要通过以下配置来完成 Tree Shaking 操作:

  • optimization.providedExports
  • optimization.usedExports
  • optimization.innerGraph
  • optimization.sideEffects

接下来,我们通过一个具体的例子对上述配置进行说明:

// src/net.js
export const HOST = 'localhost';
export const PORT  = 3000;

// src/config.js
export * from './net';

// src/utils.js
import { HOST } from './config';

function getHost() {
  return HOST;
}

export function echoHello() {
  console.log('hello');
}

export function echoHost() {
  console.log(getHost());
}

// src/index.js
import { echoHello } from './utils';

echoHello();
复制代码

上述代码片段中,我们做了以下几件事情:

  • src/net.js 中导出 HOSTPORT 变量;
  • src/config.js 中通过 export * from 语句导出 src/net.js 中的所有导出成员;
  • src/utils.js 中引入 src/config.js 的导出成员 HOST,接着定义 getHostechoHelloechoHost 函数,然后导出 echoHelloechoHost 函数;
  • src/index.js 中引入 src/utils.js 的导出函数 echoHello,然后调用该函数。

optimization.providedExports

该选项的默认值为 true;它的主要作用是为 export * from 语句生成更加高效的代码,即收集相关模块的导出成员,并将这些成员以具体化的形式导出,比如下例:

// webpack.config.js

module.exports = {
  mode: 'development',
  devtool: false,
  optimization: {
    providedExports: false,
  },
};
复制代码

根据上面的配置,我们看下 src/config.js 打包后的效果:

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/* harmony reexport (unknown) */ var __WEBPACK_REEXPORT_OBJECT__ = {};
/* harmony reexport (unknown) */ for(const __WEBPACK_IMPORT_KEY__ in _net__WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== "default") __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = () => _net__WEBPACK_IMPORTED_MODULE_0__[__WEBPACK_IMPORT_KEY__]
/* harmony reexport (unknown) */ __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
/***/ }),
复制代码

optimization.providedExports 值修改为 true,再次观察 src/config.js 打包后的效果:

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.HOST),
/* harmony export */   "PORT": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.PORT)
/* harmony export */ });
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/***/ }),
复制代码

对比打包后的代码可知,开启 optimization.providedExports 选项后,生成的代码无需遍历 _net__WEBPACK_IMPORTED_MODULE_0__ 即可明确相关模块所要导出的具体成员,其代码更加简短和高效。不过需要注意的是,即使将该选项的值设置为 false,它也不会影响 Webpack 的 Tree Shaking。

optimization.usedExports

该选项的默认值在 production 环境下为 true,其它环境下为 false;它的主要作用是收集并标注那些没有被程序上下文引用的模块导出成员,比如下例:

// webpack.config.js

module.exports = {
  mode: 'development',
  devtool: false,
  optimization: {
    providedExports: true,
    usedExports: false,
  },
};
复制代码

根据上面的配置,我们看下 src/net.jssrc/config.jssrc/utils.js 打包后的效果:

/***/ "./src/net.js":
/*!********************!*\
 !*** ./src/net.js ***!
 \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* binding */ HOST),
/* harmony export */   "PORT": () => (/* binding */ PORT)
/* harmony export */ });
const HOST = 'localhost';
const PORT  = 3000;
/***/ }),

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.HOST),
/* harmony export */   "PORT": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.PORT)
/* harmony export */ });
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/***/ }),

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.HOST),
/* harmony export */   "PORT": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.PORT)
/* harmony export */ });
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/***/ }),
复制代码

optimization.usedExports 值修改为 true,再次观察 src/net.jssrc/config.jssrc/utils.js 打包后的效果:

/***/ "./src/net.js":
/*!********************!*\
 !*** ./src/net.js ***!
 \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* binding */ HOST)
/* harmony export */ });
/* unused harmony export PORT */
const HOST = 'localhost';
const PORT  = 3000;
/***/ }),

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.HOST)
/* harmony export */ });
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/***/ }),

/***/ "./src/utils.js":
/*!**********************!*\
 !*** ./src/utils.js ***!
 \**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "echoHello": () => (/* binding */ echoHello)
/* harmony export */ });
/* unused harmony export echoHost */
/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./config */ "./src/config.js");
复制代码

对比打包后的代码可知,开启 optimization.usedExports 选项后:

  • __webpack_require__.d 中仅对需要使用的导出成员进行了定义;
  • 对没有使用到的导出成员进行了标记,比如 /* unused harmony export echoHost */

Webpack 正是通过 optimization.usedExports 选项收集使用及未使用的导出成员,对未使用的导出成员进行标记,以便后续使用 terser-webpack-plugin 等插件完成代码优化,删除掉这些未使用的代码片段。

optimization.innerGraph

该选项是 Webpack 5 新引入的特性,其默认值在 production 环境下为 true,其它环境下为 false;它的主要作用是构建内部依赖图,对模块中的标志进行分析,找出导出和引用之间的依赖关系,以便清除更多的无用代码。

比如 src/utils.js 中的导出函数 echoHost,它调用了 getHost 函数,getHost 又引用了 src/config.js 的导出成员 HOST,但在 src/index.jsechoHost 并未被引用,这种情况下,Webpack 可放心大胆地将 src/config.jssrc/net.js 中的代码逻辑移除。

// webpack.config.js

module.exports = {
  mode: 'development',
  devtool: false,
  optimization: {
    providedExports: true,
    usedExports: true,
    innerGraph: false,
  },
};
复制代码

根据上面的配置,我们看下 src/net.jssrc/config.js 打包后的效果:

/***/ "./src/net.js":
/*!********************!*\
 !*** ./src/net.js ***!
 \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* binding */ HOST)
/* harmony export */ });
/* unused harmony export PORT */
const HOST = 'localhost';
const PORT  = 3000;
/***/ }),

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "HOST": () => (/* reexport safe */ _net__WEBPACK_IMPORTED_MODULE_0__.HOST)
/* harmony export */ });
/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");
/***/ }),
复制代码

optimization.innerGraph 值修改为 true,再次观察 src/net.jssrc/config.js 打包后的效果:

/***/ "./src/net.js":
/*!********************!*\
 !*** ./src/net.js ***!
 \********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* unused harmony exports HOST, PORT */
const HOST = 'localhost';
const PORT  = 3000;

/***/ }),

/***/ "./src/config.js":
/*!***********************!*\
 !*** ./src/config.js ***!
 \***********************/
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {

/* harmony import */ var _net__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./net */ "./src/net.js");

/***/ }),
复制代码

对比打包后的代码可知,开启 optimization.innerGraph 选项后:

  • src/net.jssrc/config.js 中均没有了 __webpack_require__.d 调用;
  • src/net.js 中的 HOSTPORT 均被标记成了未使用。

通过上述标记及处理,Webpack 便可以在后续步骤中清除 src/net.jssrc/config.js 中的代码。

pure 标记

src/utils.js 中添加以下代码:

console.log('dead code');
复制代码

然后以 production 模式进行打包:

(()=>{"use strict";console.log("dead code"),console.log("hello")})();
复制代码

假如我们希望 Webpack 可以删除掉类似 console.log("dead code") 的调试语句,可通过 pure 标记来完成:

/*#__PURE__*/
console.log('dead code');
复制代码

再次以 production 模式进行打包:

(()=>{"use strict";console.log("hello")})();
复制代码

此时 console.log("dead code") 语句被移除了,这里需要注意的是,使用 pure 标记的代码片段不能具有副作用,否则将产生难以意料的异常。

optimization.sideEffects

该选项的默认值在 production 环境下为 true,其它环境下为 flag(为 true 时,Webpack 会利用 JavaScriptParser 对模块代码进行解析,找到具有副作用的代码片段,并将其记录到 ModuleGraph 对象中);它的主要作用是根据 package.json 中的 sideEffects 配置来决定 Webpack 是否可以放心大胆地对模块进行 Tree shaking 操作;它的使用涉及两个方面:

  • Webpack 中的 optimization.sideEffects 配置,用以开启副作用检测功能;
  • package.json 中的 sideEffects 配置用于告知 Webpack npm 包是否有副作用,类型为 boolstring[](当为 string[] 时,每一项为 glob 模式匹配字符串)。

比如下面的例子:

// src/utils.js
Array.prototype.sum = function() {
  return this.reduce((result, num) => (result + num), 0);
}

// src/index.js
import './utils';

console.log([1, 2, 3].sum());

// webpack.config.js
module.exports = {
  mode: 'production',
  devtool: false,
  optimization: {
    sideEffects: true,
  }
};

// package.json
{
  "sideEffects": false,
}
复制代码

上述代码片段中,我们做了以下几件事情:

  • src/utils.js 中通过 Array 添加了 sum 方法;
  • src/index.js 中引入 src/utils.js,并调用 Arraysum 方法;
  • webpack.config.js 中将 optimization.sideEffects 设置为 true
  • package.json 中将 sideEffects 设置为 false

打包后的结果如下:

(()=>{"use strict";console.log([1,2,3].sum())})();
复制代码

由于我们在 package.json 中将 sideEffects 设置为 false,这也就告诉 Webpack 可以放心大胆地进行 Tree shaking 操作,然而上述打包代码会因调用了不存在的方法而抛出异常,这就是所谓的副作用。要想正常工作,将 package.json 中的值设置为 true["./src/utils.js"](建议使用 string[],这样可以对不在配置列表中的代码进行 Tree shaking 操作)即可,修改完 package.json 中的设置再次打包可发现声明的文件并未执行 Tree shaking 操作:

(()=>{var r={555:()=>{Array.prototype.sum=function(){return this.reduce(((r,e)=>r+e),0)}}},e={};function t(o){var n=e[o];if(void 0!==n)return n.exports;var u=e[o]={exports:{}};return r[o](u,u.exports,t),u.exports}t.n=r=>{var e=r&&r.__esModule?()=>r.default:()=>r;return t.d(e,{a:e}),e},t.d=(r,e)=>{for(var o in e)t.o(e,o)&&!t.o(r,o)&&Object.defineProperty(r,o,{enumerable:!0,get:e[o]})},t.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{"use strict";t(555),console.log([1,2,3].sum())})()})();
复制代码

通过讨论可知,sideEffects 主要运用于对全局有影响的场景下,比如操作 window 对象和加载 CSS 文件。

总结

本文我们对 Webpack Tree Shaking 的使用进行了简单介绍,通过对配置 optimization.providedExportsoptimization.usedExportsoptimization.innerGraphoptimization.sideEffectspure 标记 的熟练运用,相信我们能够在日后的工作中能够根据业务需求熟练地通过 Webpack 进行代码优化。最后,祝大家快乐编码每一天。^ _ ^

猜你喜欢

转载自juejin.im/post/7078563069391175688