likes
comments
collection
share

谈谈tree-shaking

作者站长头像
站长
· 阅读数 20

什么是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时,要有几个条件:

  1. 模块系统必须为ESmodule。

  2. 在package.json文件标识sideEffect字段。

  3. 把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并不是一件容易的事情,仍有很多问题需要多方共同解决。