谈谈tree-shaking
什么是tree-shaking
上图应该可以形象地讲述了tree-shaking的原意,在前端领域中,tree-shaking指的是消除没被引用的模块代码,减少代码体积大小,以提高页面的性能。
在传统编程语言中,有个dead-code-elimination(DCE)概念,指消除程序中不可能运行到的代码,如:
if(1<0){
// dead code
}
前端领域所说的tree-shaking本质上也是DCE,尽可能把没用的js通通干掉,减少资源体积大小。
我的tree-shaking不起作用了
比较早做这个的是rollup,可以看看rollup的tree-shaking效果。webpack2之后,webpack也新增了tree-shaking功能,当时当我们都觉得webpack 666,webpack牛x的时候,发现新升级webpack之后,以下设置一顿操作: webpack.config.js:
mode: 'development',
context: path.resolve(__dirname, './'),
entry: {
index: ['./src/index.jsx'],
},
index.js:
import { square } from "./math";
import {add} from './fer-gen/lib/user';
console.log(add(1))
math.js:
export function square(a) {
return a * a;
}
export function add(a){
return a + a
}
bundle中貌似并没有tree-shaking啊,没用到的square依旧存在,webpack是不是骗子? 再看看tree-shaking的文档,其实不是,webpack做tree-shaking时,要有几个条件:
-
模块系统必须为ESmodule。
-
在package.json文件标识sideEffect字段。
-
把webpack设置为生产环境。
果然,按照上述配置后,最后的bundle中确实找不到square函数,终于起作用了!那么为什么要有上述这几点规定,下面我们来说说。
{
"presets": [
[ "env"]
]
}
模块系统必须为ESmodule
为什么要是es模块系统才行,commonjs不行吗?是的,只有es模块系统可以,原因是ESModule是静态的,依赖关系在编译时就确定了。而commonjs是node环境默认的模块系统,是动态的。而webpack只会在编译时做处理,所以只有es模块才行。 但是,在实际项目中,在进入webpack优化前,我们一般会用一系列loader对源码进行处理,其中包括神一样的babel-loader。我们按照以下的babel配置: 同样是上面的源码,webpack打包后,我们可以看到tree-shaking又失效了。看看babel转换后的:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.square = square;
exports.add = add;
function square(a) {
return a * a;
}
function add(a) {
return a + a;
}
问题是preset为env时,模块转换默认为cjs。我们把babel配置为以下即可:
{
"presets": [
[ "env",{modules:false}]
]
}
这样babel就不会把我们原来的ESmodule转换为其他模块系统。
对于ts项目,一般我们都会使用ts-loader来处理ts文件,对于ts,同样,我们需要在tsconfig.json中的module设置为'esnext'或者'es2015':
{
"compilerOptions": {
module:'esnext'
}
}
那么可能又有同学问,webpack能识别es6+语法吗? 之前不行,现在可以了~。之前做tree-shaking,代码混淆,的是uglifyjs,现在是terser,两者的区别就是terser支持es6+语法,所以现在,即使我们不使用babel,webpack依旧能够识别并打包我们的代码。
side-effect
为什么webpack要求能tree-shaking的包必须是无副作用的?
首先我们来回顾下什么side-effect,在react中我们常说纯函数组件,如:
function({name}){
return <div>{name}</div>
}
上述代码中,我们‘一般’说上面这个组件是纯的,没有引起副作用,再看下面的:
function({name}){
window.test=name;
return <div>{name}</div>
}
这时候函数就产生了副作用,因为它改变了全部变量。所以简单来说,副作用就是代码运行时所产生的:导致io,改变了其他变量的值等。
回到webpack这里说的副作用,这里是指模块的副作用,如,下面有个模块:
export a=1;
window.test=a;
这个模块就是不纯的,因为引入这个模块时,改变了全部变量。 webpack对有副作用的模块,都不会进行tree-shaking,甚至是代码混淆! 。
事实上,当前版本的webpack的tree-shaking已经相当可以了,即使我们在模块中直接改写全部变量,webpack也会把这个副作用打包进bundle,但是该模块下的其他模块依旧会被remove掉。
math.js
export function add(a){
return a+a
}
export function plus({a}){
window.test=1212
return a*a
}
plus({a:1231})
index.js
import { square,} from "./math";
console.log( square(1))
output:
[
function(e, t, n) {
'use strict';
n.r(t),
(window.test = 1212),
console.log(
(function(e) {
return e * e;
})(1)
);
}
]
那么在webpack中,还有哪些模块会被认为是有副作用的呢? 1、含有eval也被认为是不纯的,即使是在某个函数下:
export function(){
eval('var a =1')
}
2、在顶级作用域中对用一个属性赋值两次:
export const name={first:'my_name'};
name.first='my_name1';
name.first='my_name1';
这是一个十分奇怪的现象,原理上,其实属性的读与写,都会触发getter与setter,都是可能是有副作用的,然而为什么是两次setter后才被认为是有副作用?现在还没有搞明白个中原因。
tree-shaking相关配置
webpack4开始引入了模式选项,大大降低了webpack的入门门槛。生产环境下,webpack默认开启了tree-shaking.有关tree-shaking的配置如下:
optimization: {
providedExports: false,
usedExports: true,
sideEffects: true,
minimizer: [
new TerserPlugin({
cache: true,
parallel: true
})
]
},
其中,providedExports、usedExports会在webpack编译过程中标识模块的哪些方法是被导出的,sideEffects设置允许在package.json中标志没有作用的文件,而TerserPlugin是最终把无用的代码remove掉。
还有哪些问题?
如果我们遵循上面的前置条件,并且不写有副作用的代码,那么我的代码是不是可以可以得到很大的提高。然后并不!
1、tree-shaking对第三方库不管用
问题是在于,以上优化只会对项目中自己写的代码生效,引入的第三方库代码一般不是esmodule,使得tree-shaking根本起不了作用。
对于库的使用者,我们只能期待库作者提供支持es版本的库。 对于库所有者,最好是能打包出es的bundle(使用rollup打包),或者加入相关的优化手段实现按需加载(antd)。
由于历史原因,一般包入口文件都不是es模块,把入口文件直接改为es模块文件其实也会有风险,所以也有 提案 ,可以在package.json中加入es模块的入口,对于使用es模块方式引用时,使用es模块,这样对旧版本也能兼容。
2、无法shake掉class中没被引用的方法。
不管webpack也好,rollup也好,他们都只会对带有esmodule的关键词进行分析,也就是说只能shake掉没被引用的export,对于无用的classMethod,或者prototypeMethod,是一点作用都不起。比如: math.js
export class MyMath{
addOne(a){
return a+1
}
square(a){
return a*a
}
}
index.js
import {MyMath} from './math'
const math=new Math();
math.add(1);
以上代码打包后,buddle中依旧留有addOne方法,webpack对这种情况是无能为力。 针对这种情况,解决办法有两个: 1、把classMethod改为一般的函数; 2、让webpack支持method-tree-shaking; 方法一虽然能就解决,对于一般的项目显然不合适,对于库开发者来说,可以是个选择。
对于方法二,上面提到,webpack是根据es的关键词进行分析,标志出那些模块是被使用的,同理,我们是不是也可以手动给类方法加上相关的“关键词”?如:
export class MyMath{
addOne(a){
return a+1
}
/*@pure*/square(a){
return a*a
}
}
以上加入了/pure/的注释,编译时收集这些信息,优化时把无用的method移除掉,理论上可行,不失为一个解决办法,但也需要多方共同协同。
小结
可以看到,实现完美的tree-shaking并不是一件容易的事情,仍有很多问题需要多方共同解决。
转载自:https://juejin.cn/post/6956522989810614308