Webpack 性能系列五:使用 Scope Hoisting

开发 前端
Webpack 会将模块打包成一个一个分离的函数,这会造成一定程度上的代码冗余与运行性能问题.

[[435028]]

一、什么是 Scope Hoisting

默认情况下,经过 Webpack 打包后的模块资源会被组织成一个个函数形式,例如:

关于打包产物形态的更多知识,可参考前文《Webpack 原理系列八:产物转译打包逻辑》

  1. // common.js 
  2. export default "common"
  3.  
  4. // index.js 
  5. import common from './common'
  6. console.log(common); 

上例最终会被打包出形如下面结构的产物:

  1. "./src/common.js"
  2.   ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
  3.      const __WEBPACK_DEFAULT_EXPORT__ = ("common"); 
  4.      __webpack_require__.d(__webpack_exports__, { 
  5.       /* harmony export */ 
  6.       "default": () => (__WEBPACK_DEFAULT_EXPORT__) 
  7.       /* harmony export */ 
  8.     }); 
  9.   }), 
  10. "./src/index.js"
  11.   ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
  12.       var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js"); 
  13.       console.log(_common__WEBPACK_IMPORTED_MODULE_0__) 
  14.   }) 

这种结构存在两个影响到运行性能的问题:

  • 重复的函数模板代码会增大产物体积,消耗更多网络流量
  • 函数的出栈入栈需要创建、销毁作用域空间,影响运行性能

针对这些问题,自 Webpack 3 开始引入 Scope Hoisting 功能,本质上就是将符合条件的多个模块合并到同一个函数空间内,减少函数声明的模板代码与运行时频繁出入栈操作,从而打包出「体积更小」、「运行性能」更好的包。例如上述示例经过 Scope Hoisting 优化后,生成代码:

  1. ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 
  2.     ;// CONCATENATED MODULE: ./src/common.js 
  3.     /* harmony default export */ const common = ("common"); 
  4.      
  5.     ;// CONCATENATED MODULE: ./src/index.js 
  6.     console.log(common); 
  7. }) 

二、使用 Scope Hoisting

2.1 开启 Scope Hoisting 特性

Webpack 提供了三种方法开启 Scope Hoisting 功能的方法:

  • 开启 Production 模式
  • 使用 optimization.concatenateModules 配置项
  • 直接使用 ModuleConcatenationPlugin 插件

分别对应下述代码:

  1. const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); 
  2.  
  3. module.exports = { 
  4.     // 方法1: 将 `mode` 设置为 production,即可开启 
  5.     mode: "production"
  6.     // 方法2: 将 `optimization.concatenateModules` 设置为 true 
  7.     optimization: { 
  8.         concatenateModules: true
  9.         usedExports: true
  10.         providedExports: true
  11.     }, 
  12.     // 方法3: 直接使用 `ModuleConcatenationPlugin` 插件 
  13.     plugins: [new ModuleConcatenationPlugin()] 
  14. }; 

三种方法的作用原理相似,最终都会用到 ModuleConcatenationPlugin 完成模块分析与合并操作,唯一需要注意的是使用 optimization.concatenateModules 时需要将 usedExports、providedExports 同时设置为 true,标记模块的导入导出变量,才能完成合并操作。

2.2 模块合并规则

开启 Scope Hoisting 后,Webpack 会将尽可能多的模块合并到同一个函数作用域下,但合并功能一方面依赖于 ESM 静态分析能力;一方面需要确保合并操作不会造成代码冗余。因此开发者需要注意 Scope Hoisting 会在以下场景下失效:

2.2.1 非 ESM 模块

对于 AMD、CMD 一类的模块,由于模块导入导出内容的动态性,Webpack 无法确保模块合并后不会对原有的代码语义产生副作用,导致 Scope Hoisting 失效,例如:

  1. // common.js 
  2. module.exports = 'common'
  3.  
  4. // index.js 
  5. import common from './common'

上例中,由于 common.js 使用 CommonJS 导入模块内容,Scope Hoisting 失效,两个模块无法合并。

这一问题在导入 NPM 包尤其常见,由于大部分框架都会自行打包后再上传到 NPM,并且默认导出的是兼容性更佳的 CommonJS 模块方案,因而无法使用 Scope Hoisting 功能,此时可通过 mainFileds 属性尝试引入框架的 ESM 版本:

  1. module.exports = { 
  2.   resolve: { 
  3.     // 优先使用 jsnext:main 中指向的 ES6 模块化语法的文件 
  4.     mainFields: ['jsnext:main''browser''main'
  5.   }, 
  6. }; 

2.2.2 模块被多个 Chunk 引用

如果一个模块被多个 Chunk 同时引用,为避免重复打包,Scope Hoisting 同样会失效,例如:

  1. // common.js 
  2. export default "common" 
  3.  
  4. // async.js 
  5. import common from './common'
  6.  
  7. // index.js  
  8. import common from './common'
  9. 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,而是作为生成为单独的作用域,最终打包结果:

  1. "./src/common.js"
  2.  (() => { 
  3.    var __WEBPACK_DEFAULT_EXPORT__ = ("common"); 
  4.  }), 
  5. "./src/index.js"
  6.  (() => { 
  7.    var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./common */ "./src/common.js"); 
  8.    __webpack_require__.e( /*! import() */ "src_async_js").then(__webpack_require__.bind(__webpack_require__, /*! ./async */ "./src/async.js")); 
  9.  }),   

关于 Chunk 的更多内容,请参考:

  • 《Webpack 性能系列四:分包优化》
  • 《有点难的知识点:Webpack Chunk 分包规则详解》

 三、总结

默认情况下,Webpack 会将模块打包成一个一个分离的函数,这会造成一定程度上的代码冗余与运行性能问题,这一情况自 Webpack 3.0 引入 ModuleConcatenationPlugin 后,开发者可使用 Scope Hoisting 技术将多个模块合并成一个函数,减少性能问题。

 

责任编辑:姜华 来源: Tecvan
相关推荐

2021-10-25 10:23:49

Webpack 前端Tree shakin

2021-11-09 09:57:46

Webpack 前端分包优化

2021-09-27 08:16:38

Webpack 前端Cache

2021-12-15 09:21:59

Webpack 前端Sourcemap

2020-09-19 21:26:56

webpack

2015-10-20 13:11:23

JavascriptHoisting

2019-03-05 10:20:49

WebWebpack分离数据

2021-05-31 05:36:43

WebpackJavaScript 前端

2021-10-12 09:52:30

Webpack 前端多进程打包

2022-03-03 07:34:31

注解容器作用域

2019-03-15 15:00:49

Webpack构建速度前端

2021-06-28 05:59:17

Webpack 前端打包与工程化

2021-09-13 09:40:35

Webpack 前端HMR 原理

2019-03-26 10:02:16

WebpackJavascript前端

2021-08-26 10:30:29

WebpackTree-Shakin前端

2013-12-17 16:21:17

iOSiOS性能优化

2021-12-16 22:02:28

webpack原理模块化

2022-11-29 17:38:57

DockerfileARG作用域

2017-09-26 16:32:03

JavaGC分析

2022-08-30 08:50:07

Spring权限控制
点赞
收藏

51CTO技术栈公众号