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
中导出HOST
、PORT
变量; - 在
src/config.js
中通过export * from
语句导出src/net.js
中的所有导出成员; - 在
src/utils.js
中引入src/config.js
的导出成员HOST
,接着定义getHost
、echoHello
及echoHost
函数,然后导出echoHello
和echoHost
函数; - 在
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.js
、src/config.js
及 src/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.js
、src/config.js
及 src/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.js
中 echoHost
并未被引用,这种情况下,Webpack 可放心大胆地将 src/config.js
及 src/net.js
中的代码逻辑移除。
// webpack.config.js
module.exports = {
mode: 'development',
devtool: false,
optimization: {
providedExports: true,
usedExports: true,
innerGraph: false,
},
};
根据上面的配置,我们看下 src/net.js
、src/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.js
、src/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.js
、src/config.js
中均没有了__webpack_require__.d
调用;src/net.js
中的HOST
和PORT
均被标记成了未使用。
通过上述标记及处理,Webpack 便可以在后续步骤中清除 src/net.js
及 src/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 包是否有副作用,类型为bool
或string[]
(当为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
,并调用Array
的sum
方法; - 在
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.providedExports
、optimization.usedExports
、optimization.innerGraph
、optimization.sideEffects
及 pure 标记
的熟练运用,相信我们能够在日后的工作中能够根据业务需求熟练地通过 Webpack 进行代码优化。最后,祝大家快乐编码每一天。^ _ ^
转载自:https://juejin.cn/post/7078563069391175688