写于 2018.08.30webpack
webpack 2.0 开始引入 tree shaking 技术。在介绍技术以前,先介绍几个相关概念:git
AST 对 JS 代码进行语法分析后得出的语法树 (Abstract Syntax Tree)。AST语法树能够把一段 JS 代码的每个语句都转化为树中的一个节点。github
DCE Dead Code Elimination,在保持代码运行结果不变的前提下,去除无用的代码。这样的好处是:web
而所谓 Dead Code 主要包括:json
tree shaking 是 DCE 的一种方式,它能够在打包时忽略没有用到的代码。数组
tree shaking 是 rollup 做者首先提出的。这里有一个比喻:bash
若是把代码打包比做制做蛋糕。传统的方式是把鸡蛋(带壳)所有丢进去搅拌,而后放入烤箱,最后把(没有用的)蛋壳所有挑选并剔除出去。而 treeshaking 则是一开始就把有用的蛋白蛋黄放入搅拌,最后直接做出蛋糕。babel
所以,相比于排除不使用的代码,tree shaking 实际上是找出使用的代码。闭包
基于ES6
的静态引用,tree shaking 经过扫描全部 ES6 的export
,找出被import
的内容并添加到最终代码中。 webpack 的实现是把全部import
标记为有使用/无使用两种,在后续压缩时进行区别处理。由于就如比喻所说,在放入烤箱(压缩混淆)前先剔除蛋壳(无使用的import
),只放入有用的蛋白蛋黄(有使用的import
)架构
首先源码必须遵循 ES6 的模块规范 (import
&export
),若是是 CommonJS 规范 (require
) 则没法使用。
根据 Webpack 官网的提示,webpack2 支持 tree-shaking,须要修改配置文件,指定 babel 处理 js 文件时不要将 ES6 模块转成 CommonJS 模块,具体作法就是:
在 .babelrc 设置 babel-preset-es2015 的 modules 为 fasle,表示不对 ES6 模块进行处理。
// .babelrc
{
"presets": [
["es2015", {"modules": false}]
]
}
复制代码
通过测试,webpack 3 和 4 不增长这个 .babelrc 文件也能够正常 tree shaking
webpack 负责对代码进行标记,把import
&export
标记为 3 类:
import
标记为/* harmony import */
export
标记为/* harmony export ([type]) */
,其中[type]
和 webpack 内部有关,多是binding
, immutable
等等。export
标记为/* unused harmony export [FuncName] */
,其中 [FuncName]
为export
的方法名称以后在 Uglifyjs (或者其余相似的工具) 步骤进行代码精简,把没用的都删除。
全部实例代码均在 demo/webpack 目录
// index.js
import {hello, bye} from './util'
let result1 = hello()
console.log(result1)
复制代码
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
复制代码
编译后的 bundle.js 以下:
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util__ = __webpack_require__(1);
let result1 = Object(__WEBPACK_IMPORTED_MODULE_0__util__["a" /* hello */])()
console.log(result1)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = hello;
/* unused harmony export bye */
function hello () {
return 'hello'
}
function bye () {
return 'bye'
}
复制代码
注:省略了bundle.js
上边 webpack 自定义的模块加载代码,那些都是固定的。
对于没有使用的bye
方法,webpack 标记为unused harmony export bye
,可是代码依旧保留。而hello
就是正常的harmony export (immutable)
。
以后使用UglifyJSPlugin
就能够进行第二步,把bye
完全清除,结果以下:
只有hello
的定义和调用。
// index.js
import Util from './util'
let util = new Util()
let result1 = util.hello()
console.log(result1)
复制代码
// util.js
export default class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
复制代码
编译后的 bundle.js 以下:
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util__ = __webpack_require__(1);
let util = new __WEBPACK_IMPORTED_MODULE_0__util__["a" /* default */]()
let result1 = util.hello()
console.log(result1)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
/* harmony export (immutable) */ __webpack_exports__["a"] = Util;
复制代码
注意到 webpack 是对Util
类总体进行标记的(标记为被使用),而不是分别针对两个方法。也所以,最终打包的代码依然会包含bye
方法。这代表 webpack tree shaking 只处理顶层内容,例如类和对象内部都不会再被分别处理。
这主要也是因为 JS 的动态语言特性所致。若是把bye()
删除,考虑以下代码:
// index.js
import Util from './util'
let util = new Util()
let result1 = util[Math.random() > 0.5 ? 'hello', 'bye']()
console.log(result1)
复制代码
编译器并不能识别一个方法名字到底是以直接调用的形式出现 (util.hello()
) 仍是以字符串的形式 (util['hello']()
) 或者其余更加离奇的方式。所以误删方法只会致使运行出错,得不偿失。
反作用的意思某个方法或者文件执行了以后,还会对全局其余内容产生影响的代码。例如 polyfill 在各种prototype
加入方法,就是反作用的典型。(也能够看出,程序和吃药不一样,反作用不全是贬义的)
反作用总共有两种形态,是精简代码不得不考虑的问题。咱们平时在重构代码时,也应当以相相似的思惟去进行,不然总有踩坑的一天。
// index.js
import Util from './util'
console.log('Util unused')
复制代码
// util.js
console.log('This is Util class')
export default class Util {
hello () {
return 'hello'
}
bye () {
return 'bye'
}
}
Array.prototype.hello = () => 'hello'
复制代码
如上代码通过webpack + uglify
的处理后,会变成这样:
虽然Util
类被引入以后没有进行任何使用,可是不能当作没引用过而直接删除。在混合后的代码中,能够看到Util
类的本体 (export
的内容) 已经没有了,可是先后的 console.log
和对Array.prototype
的扩展依然保留。这就是编译器为了确保代码执行效果不变而作的妥协,由于它不知道这两句代码究竟是干吗的,因此他默认认定全部代码 均有 反作用。
// index.js
import {hello, bye} from './util'
let result1 = hello()
let result2 = bye()
console.log(result1)
复制代码
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
复制代码
咱们引入并调用了bye()
,可是却没有使用它的返回值result2
,这种代码能够删吗?(扪心自问,若是是你人肉重构代码,直接删掉这行代码的可能性有没有超过 90% ?)
webpack 并无删除这行代码,至少没有删除所有。它确实删除了result2
,但保留了 bye()
的调用(压缩的代码表现为Object(r.a)()
)以及bye()
的定义。
这一样是由于编译器不清楚bye()
里面究竟作了什么。若是它包含了如Array.prototye
的扩展,那删掉就又出问题了。
咱们很感谢 webpack 如此严谨,但若是某个方法就是没有反作用的,咱们该怎么告诉 webpack 让他放心大胆的删除呢?
有 3 个方法,适用于不一样的状况。
// index.js
import {hello, bye} from './util'
let result1 = hello()
let a = 1
let b = 2
let result2 = Math.floor(a / b)
console.log(result1)
复制代码
util.js 和以前相同,再也不重复。有差异的是 webpack.config.js,须要增长参数pure_funcs
,告诉webpack Math.floor
是没有反作用的,你能够放心删除:
plugins: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
pure_funcs: ['Math.floor']
}
}
})
],
复制代码
在添加了pure_funcs
配置后,原来保留的Math.floor(.5)
被删除了,达到了咱们的预期效果。
但这个方法有一个很大的局限性,在于若是咱们把 webpack 和 uglify 合并使用,通过 webpack 的代码的方法名已经被重命名了,那么在这里配置原始的方法名也就失去了意义。而例如Math.floor
这类全局方法不会重命名,才会生效。所以适用性不算太强。
webpack 4 在 package.json 新增了一个配置项叫作sideEffects
, 值为false
表示整个包都没有反作用;或者是一个数组列出有反作用的模块。详细的例子能够查看 webpack 官方提供的例子。
从结果来看,若是sideEffects
值为false
,当前包export
了 5 个方法,而咱们使用了 2 个,剩下 3 个也不会被打包,是符合预期的。但这要求包做者的自觉添加,所以在当前 webpack 4 推出不久的状况下,局限性也不算小。
webpack 3 开始加入了webpack.optimize.ModuleConcatenateModulePlugin()
,到了 webpack 4 直接做为 `mode = 'production' 的默认配置。这是对 webpack bundle 的一个优化,把原本“每一个模块包裹在一个闭包里”的状况,优化成“全部模块都包裹在同一个闭包里”的状况。自己对于代码缩小体积有很大的提高,这里也能侧面解决反作用的问题。
依然选取这样 2 个文件做为例子:
// index.js
import {hello, bye} from './util'
let result1 = hello()
let result2 = bye()
console.log(result1)
复制代码
// util.js
export function hello () {
return 'hello'
}
export function bye () {
return 'bye'
}
复制代码
在开启了 concatenateModule 功能后,打包出来的代码以下:
首先,bye()
方法的调用和本体都被消除了。
其次,hello()
方法的调用和定义被合成到了一块儿,变成直接console.log('hello')
第三就是这个功能原有的目的:代码量减小了。
这个功能的本意是把全部模块最终输出到同一个方法内部,从而把调用和定义合并到一块儿。这样像bye()
这样没有反作用的方法就能够在合并以后被轻易识别出来,并加以删除。有关这个功能更加详细的介绍能够看这篇文章