在本章节中通过一个引入 Ladash 特定模块的实例来展示 Tree-shaking 在 Webpack 中的重要作用。通过合理的使用 Tree-shaking 功能可以有效的减少打包后文件的大小,通过本实例我们也可以知道 Tree-shaking 的作用条件和范围。这样对于 Webpack 优化策略又掌握了一部分核心知识。
是否需要引入 Tree-shaking
下面是 Ladash 中对外导出的对象:
lodash.isFunction = isFunction;
lodash.isInteger = isInteger;
lodash.isLength = isLength;
lodash.isMap = isMap;
lodash.isMatch = isMatch;
lodash.isMatchWith = isMatchWith;
lodash.isNaN = isNaN;
lodash.isNative = isNative;
lodash.isNil = isNil;
lodash.isNull = isNull;
lodash.isNumber = isNumber;
lodash.isObject = isObject;
lodash.isObjectLike = isObjectLike;
lodash.isPlainObject = isPlainObject;
lodash.isRegExp = isRegExp;
lodash.isSafeInteger = isSafeInteger;
lodash.isSet = isSet;
lodash.isString = isString;
lodash.isSymbol = isSymbol;
lodash.isTypedArray = isTypedArray;
lodash.isUndefined = isUndefined;
lodash.isWeakMap = isWeakMap;
lodash.isWeakSet = isWeakSet;
这是为什么我们可以通过如下方式引入方法的原因:
import { concat, sortBy, map, sample } from 'lodash';
//lodash 其实是一个对象
但是还有一种常见的方法就是只引入我们需要的函数,如下:
import sortBy from 'lodash/sortBy';
import map from 'lodash/map';
import sample from 'lodash/sample';
之所以可以通过这种方法引用是因为在 Lodash 的 npm 包中,每一个方法都对应于一个独立的文件,并导出了该方法,例如下面就是 sortBy.js 方法的源码:
var sortBy = baseRest(function(collection, iteratees) {
if (collection == null) {
return [];
}
var length = iteratees.length;
if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {
iteratees = [];
} else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {
iteratees = [iteratees[0]];
}
return baseOrderBy(collection, baseFlatten(iteratees, 1), []);
});
module.exports = sortBy;
注意一点就是,通过后者来导入我们需要的文件比前者全部导入的文件要小的多。上面已经说了原因,即后者将每一个方法都存放在一个独立的文件中,从而可以按需导入,所以文件也就比较小了。具体可以查看这里来学习如何减少 bundle.js 的大小。
当然,如果使用了 Webpack 3 的 Tree-shaking,那么就不需要考虑这个情况了。Tree-shaking 会让没用的代码在打包的时候直接被剔除。但是,请注意,Tree-shaking 的功能要生效必须满足一定的条件,即必须是 ES6 模块
Webpack 引入 Tree-shaking 功能
Webpack 如何使用 Tree-shaking
为了让 Webpack 2 支持 Tree-shaking 功能,需要对 wcf 的 babel 配置进行修改,其中修改最重要的一点就是去掉 babel-preset-es2015 ,而采用 plugin 处理。在 plugin 处理的时候还需要去掉下面的插件:
require.resolve("babel-plugin-transform-es2015-modules-amd"),
//转化为 amd 格式,define 类型
require.resolve("babel-plugin-transform-es2015-modules-commonjs"),
//转化为 commonjs 规范,得到:exports.default = 42,export.name="罄天"
require.resolve("babel-plugin-transform-es2015-modules-umd"),
//umd规范
采用 babel-plugin-transform-es2015-modules-commonjs 以后,代码如下:
//imported.js
export function foo() {
return 'foo';
}
export function bar() {
return 'bar';
}
//下面是 index.js
import {foo} from './imported';
let elem = document.getElementById('output');
elem.innerHTML = `Output: ${foo()}`;
会被 Webpack 转化为如下的形式:
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.foo = foo;
exports.bar = bar;
//都转化为 commonjs 规范了
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
var _imported = __webpack_require__(0);
var elem = document.getElementById('output');
elem.innerHTML = 'Output: ' + (0, _imported.foo)();
/***/ })
/******/ ]);
所以,我们没有用到的 bar 方法也被引入了。而如果引入 babel-plugin-transform-es2015-modules-amd,打包代码就会得到如下的内容:
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [exports], __WEBPACK_AMD_DEFINE_RESULT__ = function (exports) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.foo = foo;
exports.bar = bar;
//没有用到的 bar 方法也被导出了
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(0)], __WEBPACK_AMD_DEFINE_RESULT__ = function (_imported) {
'use strict';
var elem = document.getElementById('output');
elem.innerHTML = 'Output: ' + (0, _imported.foo)();
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
/***/ })
/******/ ]);
而如果引入 babel-plugin-transform-es2015-modules-umd 也会面临同样的问题,所以我们应该去掉上面三个插件,即不再使用 amd/cmd/umd 规范打包,而使用 ES6 原生模块打包策略。让 ES6 模块不受 Babel 预设(preset)的影响。Webpack 认识 ES6 模块,只有当保留 ES6 模块语法时才能够应用 Tree-shaking。如果将其转换为 CommonJS 语法,Webpack 不知道哪些代码是使用过的,哪些不是(就不能应用 Tree-shaking 了)。最后,Webpack 将把它们转换为 CommonJS语法。最终得到的 babel 默认配置就是如下的内容:
function getDefaultBabelConfig() {
return {
cacheDirectory: tmpdir(),
//We must set!
presets: [
require.resolve('babel-preset-react'),
// require.resolve('babel-preset-es2015'),
//(1)这个必须去掉
require.resolve('babel-preset-stage-0'),
],
plugins: [
require.resolve("babel-plugin-transform-es2015-template-literals"),
require.resolve("babel-plugin-transform-es2015-literals"),
require.resolve("babel-plugin-transform-es2015-function-name"),
require.resolve("babel-plugin-transform-es2015-arrow-functions"),
require.resolve("babel-plugin-transform-es2015-block-scoped-functions"),
require.resolve("babel-plugin-transform-es2015-classes"),
//这里会转化class
require.resolve("babel-plugin-transform-es2015-object-super"),
require.resolve("babel-plugin-transform-es2015-shorthand-properties"),
require.resolve("babel-plugin-transform-es2015-computed-properties"),
require.resolve("babel-plugin-transform-es2015-for-of"),
require.resolve("babel-plugin-transform-es2015-sticky-regex"),
require.resolve("babel-plugin-transform-es2015-unicode-regex"),
require.resolve("babel-plugin-syntax-object-rest-spread"),
require.resolve("babel-plugin-transform-es2015-parameters"),
require.resolve("babel-plugin-transform-es2015-destructuring"),
require.resolve("babel-plugin-transform-es2015-block-scoping"),
require.resolve("babel-plugin-transform-es2015-typeof-symbol"),
[
require.resolve("babel-plugin-transform-regenerator"),
{ async: false, asyncGenerators: false }
],
// require.resolve("babel-plugin-add-module-exports"),
// 交给 Webpack 2 处理,可以删除
require.resolve("babel-plugin-check-es2015-constants"),
require.resolve("babel-plugin-syntax-async-functions"),
require.resolve("babel-plugin-syntax-async-generators"),
require.resolve("babel-plugin-syntax-class-constructor-call"),
require.resolve("babel-plugin-syntax-class-properties"),
require.resolve("babel-plugin-syntax-decorators"),
require.resolve("babel-plugin-syntax-do-expressions"),
require.resolve("babel-plugin-syntax-dynamic-import"),
require.resolve("babel-plugin-syntax-exponentiation-operator"),
require.resolve("babel-plugin-syntax-export-extensions"),
require.resolve("babel-plugin-syntax-flow"),
require.resolve("babel-plugin-syntax-function-bind"),
require.resolve("babel-plugin-syntax-jsx"),
require.resolve("babel-plugin-syntax-trailing-function-commas"),
require.resolve("babel-plugin-transform-async-generator-functions"),
require.resolve("babel-plugin-transform-async-to-generator"),
require.resolve("babel-plugin-transform-class-constructor-call"),
require.resolve("babel-plugin-transform-class-properties"),
require.resolve("babel-plugin-transform-decorators"),
require.resolve("babel-plugin-transform-decorators-legacy"),
require.resolve("babel-plugin-transform-do-expressions"),
require.resolve("babel-plugin-transform-es2015-duplicate-keys"),
require.resolve("babel-plugin-transform-es2015-spread"),
require.resolve("babel-plugin-transform-exponentiation-operator"),
require.resolve("babel-plugin-transform-export-extensions"),
// require.resolve("babel-plugin-transform-es2015-modules-amd"),
// require.resolve("babel-plugin-transform-es2015-modules-commonjs"),
// require.resolve("babel-plugin-transform-es2015-modules-umd"),
// (2)去掉这个
require.resolve("babel-plugin-transform-flow-strip-types"),
require.resolve("babel-plugin-transform-function-bind"),
require.resolve("babel-plugin-transform-object-assign"),
require.resolve("babel-plugin-transform-object-rest-spread"),
require.resolve("babel-plugin-transform-proto-to-assign"),
require.resolve("babel-plugin-transform-react-display-name"),
require.resolve("babel-plugin-transform-react-jsx"),
require.resolve("babel-plugin-transform-react-jsx-source"),
require.resolve("babel-plugin-transform-runtime"),
require.resolve("babel-plugin-transform-strict-mode"),
]
};
}
具体文件内容可以点击 wcf 打包 babel 配置。当然也可以使用下面方式告诉 babel 预设不转换模块:
{
"presets": [
["env", {
"loose": true,
"modules": false
}]
]
}
这种方式要简单的多。但是这种方式会存在副作用,即无法移除多余的类声明。在使用 ES6 语法定义类时,类的成员函数会被添加到属性 prototype,没有什么方法能完全避免这次赋值,所以 Webpack 会认为我们添加到 prototype 上方法的操作也是对类的一种使用,导致无法移除多余的类声明,编译过程阻止了对类进行 Tree-shaking ,它仅对函数起作用。UglifyJS 不能够分辨它仅仅是类声明,还是其他有副作用的操作,因为 UglifyJS 不能做控制流分析。
Webpack 的 Tree-shaking 标记 VS rollup 标记区别
移除未使用代码(Dead code elimination) VS 包含已使用代码(live code inclusion)
Webpack 仅仅标记未使用的代码而不移除,并且不将其导出到模块外。它拉取所有用到的代码,将剩余的(未使用的)代码留给像 UglifyJS 这类压缩代码的工具来移除。UglifyJS 读取打包结果,在压缩之前移除未使用的代码。而 Rollup 不同,它的打包结果只包含运行应用程序所必需的代码。打包完成后的输出并没有未使用的类和函数,压缩仅涉及实际使用的代码。
基于 babel-minify-webpack-plugin(即 babili-webpack-plugin)移除多余的类声明
能将 ES6 代码编译为 ES5,移除未使用的类和函数,这就像 UglifyJS 已经支持 ES6 一样。babel-minify 会在编译前删除未使用的代码
。在编译为 ES5 之前,很容易找到未使用的类,因此 Tree-shaking 也可以用于类声明,而不再仅仅是函数。若看下 babili-webpack-plugin 的代码,会看到下面两句:
import { transform } from 'babel-core';
import babelPresetMinify from 'babel-preset-minify';
首先是babel-preset-minify,可以看到其内部会调用如 babel-plugin-minify-dead-code-elimination 、 babel-plugin-minify-type-constructors 等来判断哪些代码没有被引用,进而可以在代码没有被编译为 ES5 之前把它移除掉。而 babel-core 就是负责把处理后的 ES6 代码继续编译为 ES5 代码。
所以,我们只需用 babel-minify-webpack-plugin 替换 UglifyJS ,然后删除 babel-loader (该 plugin 自己会处理 ES6 代码,但是 jsx 处理需要自己添加 preset )即可。另一种方式是将babel-preset-minify作为 Babel 的预设,仅使用 babel-loader(移除 UglifyJS 插件,因为 babel-preset-minify 已经压缩完成)。推荐使用第一种(插件的方式),因为当编译器不是 Babel(如 Typescript)时,它也能生效。
module: {
rules: []
},
plugins: [
new BabiliPlugin()
//替代 UglifyJS,它可以移除 ES6 的多余类声明
]
我们需要将 ES6+ 代码传给 babel-minify ,否则它不会移除(未使用的)类。所以,这种方式就要求所有的第三方包都必须有 ES6 的代码发布,否则无法移除。
######### 目前wcf没有引入 babili-webpack-plugin
这种情况下我们依然会对类的代码打包成为 ES5,然后交给 UglifyJS 处理,比如下面的例子:
//imported.js
export function foo() {
return 'foo';
}
export function bar() {
return 'bar';
}
export function ql(){
return 'ql'
}
export class Test{
toString(){
return 'test';
}
}
export class Test1{
toString(){
return 'test1';
}
}
//index.js
import {foo} from './imported';
let elem = document.getElementById('app');
elem.innerHTML = `Output: ${foo()}`;
打包后的结果如下:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */
__webpack_exports__["a"] = foo;
/* unused harmony export bar */
/* unused harmony export ql */
/* unused harmony export Test */
/* unused harmony export Test1 */
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__ = __webpack_require__(8);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass__ = __webpack_require__(9);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass__);
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
function ql() {
return 'ql';
}
var Test = function () {
function Test() {
__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default()(this, Test);
}
__WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default()(Test, [{
key: 'toString',
value: function toString() {
return 'test';
}
}]);
return Test;
}();
var Test1 = function () {
function Test1() {
__WEBPACK_IMPORTED_MODULE_0_babel_runtime_helpers_classCallCheck___default()(this, Test1);
}
__WEBPACK_IMPORTED_MODULE_1_babel_runtime_helpers_createClass___default()(Test1, [{
key: 'toString',
value: function toString() {
return 'test1';
}
}]);
return Test1;
}();
})
此时通过查看harmony export
部分,我们知道 Webpack 导出的仅仅是用到的 foo 模块而已,而其他的不管是多余的函数声明还是多余的类声明都是被标记为无用代码('unused'
)。通过这种方式打包,经过 UglifyJS 处理就会将类 Test 1、Test 2 的代码移除,其实事实并不是这样,经过 UglifyJS 处理后多余的函数是没有了,但是多余的类声明打包成的函数代码依然存在!依然存在!依然存在!
终极解决方法:使用babel-minify-webpack-plugin,即 babili-webpack-plugin。完整实例代码可以参考这里,而目前wcf没有采用这种策略,所以多余的 class 是无法去除的。目前,我觉得这种策略是可以接受的,因为第三方发布的包很少是使用 class 发布,而都是编译为 ES5 代码后发布的,所以通过 UglifyJS 这种策略已经足够了。当然,也可以使用babel-preset-minify来将代码压缩作为你的预设,这种方式在独立封装自己的打包工具的时候比较有用,它是所有 babel 代码压缩插件的集合。
Tree-shaking 的局限性
这一部分都是自己的理解,但是基于这样一个事实:
import {sortBy} from "lodash";
通过 import 引入 sortBy 方法以后,以为仅仅是引入了该方法而已,但是实际上把 concat 等函数都引入了。因为 import 是基于 ES6 的静态语法分析,而 lodash 第三方包导出的时候并不是基于 ES6 的 import/export 机制,代码如下:
var _ = runInContext();
if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) {
root._ = _;
define(function() {
return _;
});
}
else if (freeModule) {
// Export for Node.js.
(freeModule.exports = _)._ = _;
// Export for CommonJS support.
freeExports._ = _;
}
else {
// Export to the global object.
root._ = _;
}
}.call(this));
所以,我们在引入一个 lodash 模块的时候应该使用下面的模式:
import sortBy from 'lodash/sortBy';