[TOC]

不采用Scope Hoisting的问题

在不开启Scope Hoisting时,构建后的代码存在大量的闭包代码。

带来的问题:

  • 大量函数闭包包裹代码,导致体积增大(模块越多越明显);
  • 运行代码时创建的函数作用域变多,内存开销变大。

模块转换分析

  • 被webpack转换后的模块会带上一层包裹;
  • import会被转换成__webpack_require__

webpack模块机制分析

  • 打包出来的是一个IIFE(匿名闭包);
  • modules是一个数组,每一项是一个模块初始化函数;
  • __webpack_require用来加载模块,返回module.exports;
  • 通过__webpack_require__(0)来启动程序。这里是0是moduleId,一般就是入口文件的id。

Scope Hoisting(作用域提升)原理

原理:将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突。

通过Scope Hoisting可以减少函数声明代码和内存开销。

Scope Hoisting可以让Webpack打包出来的代码文件更小、运行的更快,它又译作作用域提升,是在Webpack3中新推出的功能。下面来详细介绍Scope Hoisting

认识Scope Hoisting

Scope Hoisting和tree shaking一样都是从rollup借鉴而来的。

让我们先来看看在没有Scope Hoisting之前Webpack的打包方式。

假如现在有两个文件分别是util.js:

export default 'Hello, Webpack';
1

入口文件main.js:

import str from './util.js';
console.log(str);
1
2

以上源码用Webpack打包后输出中的部分代码如下:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);
    console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]);
  }),
  (function (module, __webpack_exports__, __webpack_require__) {
    __webpack_exports__["a"] = ('Hello,Webpack');
  })
]
1
2
3
4
5
6
7
8
9

在开启Scope Hoisting后,同样的源码输出的部分代码如下:

[
  (function (module, __webpack_exports__, __webpack_require__) {
    var util = ('Hello,Webpack');
    console.log(util);
  })
]
1
2
3
4
5
6

可以看出开启Scope Hoisting后,函数申明由两个变成了一个,util.js中定义的内容被直接注入到了main.js对应的模块中。这样做的好处是:

  • 代码体积更小,因为函数申明语句会产生大量的代码;
  • 代码在运行时因为创建的函数作用域变少了,所以内存开销也变小了。

Scope Hoisting的实现原理其实很简单:分析出模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。

由于Scope Hoisting需要分析出模块之间的依赖关系,因此源码必须采用ES6模块化语句,不然它将无法生效。

使用Scope Hoisting

Scope Hoisting是在webpack3中引入的。

webpack3中用法

Webpack中使用Scope Hoisting非常简单,因为这是Webpack内置的功能,只需要配置插件module-concatenation-plugin,相关代码如下:

const webpack = require('webpack');
module.exports = {
  plugins: [
    // 开启 Scope Hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
  ],
};
1
2
3
4
5
6
7

同时,考虑到Scope Hoisting依赖源码时需采用ES6模块化语法,还需要配置 mainFields。原因在使用TreeShaking中提到过:因为大部分Npm中的第三方库采用了CommonJS语法,但部分库会同时提供ES6模块化的代码,所以为了充分发挥Scope Hoisting的作用,需要增加以下配置:

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

对于采用了非ES6模块化语法的代码,Webpack会降级处理且不使用Scope Hoisting优化,为了知道Webpack对哪些代码做了降级处理,我们可以在启动 Webpack时带上--display-optimization-bailout参数,这样在输出日志中就会包含类似如下的日志:

[0] ./main.js + 1 modules 80 bytes {0} [built]
    ModuleConcatenation bailout: Module is not an ECMAScript module
1
2

其中的ModuleConcatenation bailout告诉了我们哪个文件因为什么原因导致了降级处理。

也就是说,要开启Scope Hoisting并发挥最大作用的配置如下:

const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
  resolve: {
    // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
    mainFields: ['jsnext:main', 'browser', 'main']
  },
  plugins: [
    // 开启 Scope Hoisting
    new ModuleConcatenationPlugin(),
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12

webpack4.x用法

webpack4.x版本在生产环境下默认开启Scope Hoisting必须是ES6语法,不支持commonjs语法

// 源代码如下
import { helloworld } from './helloworld';
import {common} from '../../common/';

document.write(helloworld());
1
2
3
4
5
// 不开启Scope Hoisting,打包后的代码如下
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _common___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);

document.write(Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])());

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
function helloworld() {
  return 'Hello webpack';
}

/***/ })
/******/ ]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

手动开启Scope Hoisting:

new webpack.optimize.ModuleConcatenationPlugin()
1
// 打包结果如下:
/******/ ({

/***/ 15:
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/index/helloworld.js
function helloworld() {
  return 'Hello webpack';
}
// EXTERNAL MODULE: ./common/index.js
var common = __webpack_require__(0);

// CONCATENATED MODULE: ./src/index/index.js

document.write(helloworld());

/***/ })

/******/ });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23