webpack v4 开始新增了一个 sideEffects
特性,通过给 package.json 加入 sideEffects 声明该 包/模块 是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。
基于我们对 side effect 的常规理解,我们可以认为,只要我们确定当前包里的模块不包含副作用,然后将发布到 npm 里的包标注为 sideEffects: false
,我们就能为使用方提供更好的打包体验。原理是 webpack 能将标记为 side-effects-free 的包由 import {a} from xx
转换为 import {a} from 'xx/a'
,从而自动修剪掉不必要的 import
,作用同 babel-plugin-import
。
Tree Shaking 与副作用
Tree-Shaking在前端界由rollup首先提出并实现,后续webpack在2.x版本也借助于UglifyJS实现了。自那以后,在各类讨论优化打包的文章中,都能看到Tree-Shaking的身影。
Tree-Shaking的原理
ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。
分析程序流,判断哪些变量未被使用、引用,进而删除此代码。
很好,原理非常完美,那为什么有时候我们项目里面多余的的代码又删不掉呢?
先说原因:都是副作用的锅!
副作用
了解过函数式编程的同学对副作用这词肯定不陌生。它大致可以理解成:一个函数会、或者可能会对函数外部变量产生影响的行为。
举个例子,比如这个函数:
function go (url) {
window.location.href = url
}
这个函数修改了全局变量location,甚至还让浏览器发生了跳转,这就是一个有副作用的函数。
// componetns.js
export class Person {
constructor ({ name }) {
this.className = 'Person'
this.name = name
}
getName () {
return this.name
}
}
export class Apple {
constructor ({ model }) {
this.className = 'Apple'
this.model = model
}
getModel () {
return this.model
}
}
// main.js
import { Apple } from './components'
const appleModel = new Apple({
model: 'IphoneX'
}).getModel()
console.log(appleModel)
很显然这个Person类是无用的代码
而为什么有时候别的工具,比如rollup在线repl尝试了下tree-shaking,也确实删掉了无用的代码
而使用webpack打包工具却不能进行有效的代码消除呢?
答案是:babel编译 + webpack打包
在这边贴一个链接,是有关于详细介绍babel编译 + webpack打包是怎么让你无效的代码消除不掉的。你的Tree-Shaking并没什么卵用
如果不想看文章的话,这边直接简单说一下原理:babel编译会使得Person类被封装成了一个IIFE(立即执行函数),然后返回一个构造函数,在这边就产生了一个副作用。
这边有个Issues,IIFE 中的类声明被视为副作用
当我在 IIFE 中声明一个类,但没有使用类时,它不会被 UglifyJS 剥离,因为它被认为是副作用。
var V6Engine = (function () {
function V6Engine() {
}
V6Engine.prototype.toString = function () {
return 'V6';
};
return V6Engine;
}());
编译时收到这样的警告:WARN: Side effects in initialization of unused variable V6Engine [./dist/car.bundle.js:74,4]
下面给出的回复:Uglify 没做执行程序流分析。它并不会因为你注意到的副作用而删除代码。你要是想弄个完善一点的摇树,去隔壁rollup呗!
issue中总结下几点关键信息:
函数的参数若是引用类型,对于它属性的操作,都是有可能会产生副作用的。因为首先它是引用类型,对它属性的任何修改其实都是改变了函数外部的数据。其次获取或修改它的属性,会触发getter
或者setter
,而getter
、setter
是不透明的,有可能会产生副作用。
uglify没有完善的程序流分析。它可以简单的判断变量后续是否被引用、修改,但是不能判断一个变量完整的修改过程,不知道它是否已经指向了外部变量,所以很多有可能会产生副作用的代码,都只能保守的不删除。
rollup有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。
但这已经是很久之前的版本问题,现在的webpack tree shaking
已经做了很多的优化,足够的程序流分析进行tree shaking
webpack 的 tree shaking
的作用是可以将未被使用的 exported member
标记为 unused
同时在将其 re-export
的模块中不再 export
。说起来很拗口,看代码:
// a.js
export function a() {}
// b.js
export function b(){}
// package/index.js
import a from './a'
import b from './b'
export { a, b }
// app.js
import {a} from 'package'
console.log(a)
当我们以 app.js 为 entry
时,经过摇树后的代码会变成这样:
// a.js
export function a() {}
// b.js 不再导出 function b(){}
function b() {}
// package/index.js 不再导出 b 模块
import a from './a'
import './b'
export { a }
// app.js
import {a} from 'package'
console.log(a)
配合 webpack 的 scope hoisting
和 uglify
之后,b 模块的痕迹会被完全抹杀掉。
但是如果 b 模块中添加了一些副作用,比如一个简单的 log:
// b.js
export function b(v) { reutrn v }
console.log(b(1))
webpack 之后会发现 b 模块内容变成了:
// b.js
console.log(function (v){return v}(1))
虽然 b 模块的导出是被忽略了,但是副作用代码被保留下来了。
由于目前 transformer
转换后可能引入的各种奇怪操作引发的副作用,很多时候我们会发现就算有了 tree shaking 我们的 bundle size
还是没有明显的减小。
而通常我们期望的是 b 模块既然不被使用了,其中所有的代码应该不被引入才对。
这个时候 sideEffects
的作用就显现出来了:如果我们引入的 包/模块 被标记为 sideEffects: false
了,那么不管它是否真的有副作用,只要它没有被引用到,整个 模块/包 都会被完整的移除。
以 mobx-react-devtool 为例,我们通常这样去用:
import DevTools from 'mobx-react-devtools';
class MyApp extends React.Component {
render() {
return (
<div>
...
{ process.env.NODE_ENV === 'production' ? null : <DevTools /> }
</div>
);
}
}
这是一个很常见的按需导入场景,然而在没有 sideEffects: false
配置时,即便 NODE_ENV
设为 production
,打包后的代码里依然会包含 mobx-react-devtools 包,虽然我们没使用过其导出成员,但是 mobx-react-devtools 还是会被 import,因为里面“可能”会有副作用。
但当我们加上 sideEffects false 之后,tree shaking 就能安全的把它从 bundle 里完整的移除掉了。
sideEffects 的使用场景
上面也说到,通常我们发布到 npm 上的包很难保证其是否包含副作用(可能是代码的锅可能是 transformer 的锅),但是我们基本能确保这个包是否会对包以外的对象产生影响,比如是否修改了 window 上的属性,是否复写了原生对象方法等。如果我们能保证这一点,其实我们就能知道整个包是否能设置 sideEffects: false
了,至于是不是真的有副作用则并不重要,这对于 webpack 而言都是可以接受的。
这也就能解释为什么能给 vue 这个本身充满副作用的包加上 sideEffects: false
了。
所以其实 webpack 里的 sideEffects: false
的意思并不是我这个模块真的没有副作用,而只是为了在摇树时告诉 webpack:我这个包在设计的时候就是期望没有副作用的,即使他打完包后是有副作用的。