Webpack 性能系列五:使用 Scope Hoisting
一、什么是 Scope Hoisting
默认情况下,经过 Webpack 打包后的模块资源会被组织成一个个函数形式,例如:
关于打包产物形态的更多知识,可参考前文《Webpack 原理系列八:产物转译打包逻辑》
// common.js
export default "common";
// index.js
import common from './common';
console.log(common);
上例最终会被打包出形如下面结构的产物:
"./src/common.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
const __WEBPACK_DEFAULT_EXPORT__ = ("common");
__webpack_require__.d(__webpack_exports__, {
/* harmony export */
"default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */
});
}),
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js");
console.log(_common__WEBPACK_IMPORTED_MODULE_0__)
})
这种结构存在两个影响到运行性能的问题:
重复的函数模板代码会增大产物体积,消耗更多网络流量 函数的出栈入栈需要创建、销毁作用域空间,影响运行性能
针对这些问题,自 Webpack 3 开始引入 Scope Hoisting 功能,本质上就是将符合条件的多个模块合并到同一个函数空间内,减少函数声明的模板代码与运行时频繁出入栈操作,从而打包出「体积更小」、「运行性能」更好的包。例如上述示例经过 Scope Hoisting 优化后,生成代码:
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
;// CONCATENATED MODULE: ./src/common.js
/* harmony default export */ const common = ("common");
;// CONCATENATED MODULE: ./src/index.js
console.log(common);
})
二、使用 Scope Hoisting
2.1 开启 Scope Hoisting 特性
Webpack 提供了三种方法开启 Scope Hoisting 功能的方法:
开启 Production 模式 使用 optimization.concatenateModules
配置项直接使用 ModuleConcatenationPlugin
插件
分别对应下述代码:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
// 方法1: 将 `mode` 设置为 production,即可开启
mode: "production",
// 方法2: 将 `optimization.concatenateModules` 设置为 true
optimization: {
concatenateModules: true,
usedExports: true,
providedExports: true,
},
// 方法3: 直接使用 `ModuleConcatenationPlugin` 插件
plugins: [new ModuleConcatenationPlugin()]
};
三种方法的作用原理相似,最终都会用到 ModuleConcatenationPlugin
完成模块分析与合并操作,唯一需要注意的是使用 optimization.concatenateModules
时需要将 usedExports
、providedExports
同时设置为 true,标记模块的导入导出变量,才能完成合并操作。
2.2 模块合并规则
开启 Scope Hoisting 后,Webpack 会将尽可能多的模块合并到同一个函数作用域下,但合并功能一方面依赖于 ESM 静态分析能力;一方面需要确保合并操作不会造成代码冗余。因此开发者需要注意 Scope Hoisting 会在以下场景下失效:
2.2.1 非 ESM 模块
对于 AMD、CMD 一类的模块,由于模块导入导出内容的动态性,Webpack 无法确保模块合并后不会对原有的代码语义产生副作用,导致 Scope Hoisting 失效,例如:
// common.js
module.exports = 'common';
// index.js
import common from './common';
上例中,由于 common.js
使用 CommonJS 导入模块内容,Scope Hoisting 失效,两个模块无法合并。
这一问题在导入 NPM 包尤其常见,由于大部分框架都会自行打包后再上传到 NPM,并且默认导出的是兼容性更佳的 CommonJS 模块方案,因而无法使用 Scope Hoisting 功能,此时可通过 mainFileds
属性尝试引入框架的 ESM 版本:
module.exports = {
resolve: {
// 优先使用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
};
2.2.2 模块被多个 Chunk 引用
如果一个模块被多个 Chunk 同时引用,为避免重复打包,Scope Hoisting 同样会失效,例如:
// common.js
export default "common"
// async.js
import common from './common';
// index.js
import common from './common';
import("./async");
上例中,入口 index.js
以异步引用方式导入 async.js
模块,同时 async.js
与 index.js
都依赖于 common.js
模块,根据 Chunk 的运行规则, async.js
会被处理为单独的 Chunk ,这就意味着 common.js
模块同时被 index.js
对应的 Initial Chunk 与 async.js
对应的 Async Chunk 引用,此时 Scope Hoisting 失效,common.js
无法被合并入任一 Chunk,而是作为生成为单独的作用域,最终打包结果:
"./src/common.js":
(() => {
var __WEBPACK_DEFAULT_EXPORT__ = ("common");
}),
"./src/index.js":
(() => {
var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js");
__webpack_require__.e( /*! import() */ "src_async_js").then(__webpack_require__.bind(__webpack_require__, /*! ./async */ "./src/async.js"));
}),
❝关于 Chunk 的更多内容,请参考:
❞
三、总结
默认情况下,Webpack 会将模块打包成一个一个分离的函数,这会造成一定程度上的代码冗余与运行性能问题,这一情况自 Webpack 3.0 引入 ModuleConcatenationPlugin
后,开发者可使用 Scope Hoisting 技术将多个模块合并成一个函数,减少性能问题。