你真的了解 tree shaking 么?

1,045 阅读7分钟

什么是 Tree shaking?

Tree shaking 的概念最早是由 Rollup 工具提出来的,再后来 webpack2 也实现了相应的功能。 MDN 定义:

移除 JavaScript 上下文中的未引用代码 (dead-code) 行为的术语

就好比一颗果树,有很多的分树枝,但是有一些不结果,有的结果,那我们希望保留结果的树枝,剪掉不结果的树枝。(如果你非要杠现实情况,那么就是你对。:))

其实现的根本是 ES6 模块语法静态结构

  1. ES6 规范 :规定只能在顶部 import 依赖文件,外部模块使用关键字 export 进行导出。它的特性是静态引入,动态编译。
import {funa} from 'xxx';

if(true){
  let res = funa();
}
  1. commonJS 是动态引入,当我们某个功能或者函数需要加载外部文件的时候,我们在逻辑中 require('xxx') 引入外部的模块,并在运行的时候将其加载进来。
if(true){
  let res =  require("xxx").funa();
}

这就导致我们的编译器在处理依赖过程中变得困难。这也是为何只有 ES6 规范才可以 Tree shaking的原因。

其实在 Rollup 提出 Tree shaking 概念之前,市场上已经有相应的概念,叫做Dead-code elimination 有兴趣的可以看一下。

Rollup 的贡献者 Rich_Harris 这样评价 DCE 与 tree-shaking

Bad analogy time: imagine that you made cakes by throwing whole eggs into the mixing bowl and smashing them up, instead of cracking them open and pouring the contents out. Once the cake comes out of the oven, you remove the fragments of eggshell, except that’s quite tricky so most of the eggshell gets left in there.

类比我们要做一个蛋糕,DCE 是在蛋糕烘焙结束的时候才将鸡蛋壳挑拣出来,而 Tree shaking 是在开始的时候,将鸡蛋壳挑拣出来。Rather than excluding dead code, we’re including live code. rollup 并不是不包括死代码,是包括实时代码。说直白一些,DCE 是编译器去除掉未执行的方法,而 Tree shaaking 是去掉未用到的方法。但是两者的核心价值都是精简代码。

为什么需要使用 Tree shaking?

随着业务的发展,技术的前进,我们在实际开发中会使用越来越多成熟的轮子、成熟的第三方包,如果我们不进行 Tree shaking,我们构建出来的包会越来越大,在网页请求静态资源的时候,就会浪费带宽,影响加载速度,进而影响用户的体验。

Tree Shaking 效果

我们通过 Rollup 来看一下Tree shaking的效果。

首先我们搭建一个本地项目,我本地通过 Pnpm + Rush 来搭建,方便创建 Demo

首先我们在项目下建立一个 index.js 作为我们的入口文件.

const test = 123;
const test1 =11
console.log(test);

再创建一个 rollup.config.js 作为 Rollup 构建的配置文件。

export default {
    input: "./index.js",
    output: [

        {
            file: "./dist/bundle.js",
            format: "es",
        },
    ],
};

我们在 package.jsonscript 中添加一条命令: build: rollup -c

我们看构建的结果

rollup.gif

我们左边是源代码,右边是构建的结果,我们可以看到方法 aaa 在没有被使用的时候,Rollup 会自动帮助我们自动删除掉。

image.png

但在实际使用 Tree shaking 的过程中也会发现,有的时候并没有按照我们的预期删除掉无用的代码。如上图,这里我们不得不提一下副作用的概念。

副作用

如果我们有一个函数,这个函数依赖了当前函数作用域之外的变量或者方法,我们就可以理解为这个函数不是一个纯函数,也就是说它存在副作用。

同样在 Rollup 处理我们代码的时候,也不能够十分精确的判断出某一个模块是不是一定没有副作用.

以上面的图为例,我们的函数使用了其作用域之外的变量,Rollup 就会认为这是一个存在副作用的函数。

接下来我们看一下几种存在副作用的场景:

  1. 我们上面提到的 函数使用了当前作用域之外的因素

image.png 函数 fun 使用了 age,那么 fun 是一个存在副作用的函数。

  1. 经过 babel 编译以后的代码。

image.png 直接使用,可以被 Rollup 删除掉。经过 babel 处理以后:babel处理后的在线地址

babel 编译后的效果

image.png

经过 Rollup 处理后的

image.png 我们可以看到,编译后的代码没有办法被 Tree shaking 掉。其实这也是好多时候我们构建完的第三方包(组件库),在实际使用中不能被优化的原因之一。
在这里提到了类,那我们就拓展的聊一下,Tree shaking 在对于类方法的处理上,即便是没有调用类的方法,优化算法也不会将类的方法去 Tree shaking,这是符合优化算法的预期。详情可以看 issues

image.png 3. 使用全局作用域变量

image.png

如何生产避免副作用?

1、webpackpackage.json 中增加了 sideEffects 的字段,来告诉编译器,当前的包是否包含副作用。 它接受三种 value

  • 如果是 false 代表当前包中都不存在副作用。

  • 如果是数组,可以将指定的文件保留副作用,不包括的默认没有副作用。

    比如我们在代码中引入了 css 文件,如果不指定的话,会被当作无用代码,剔除掉。

    image.png

    //pacakge.json
    sideEffects: ['*.css']
    

    sideEffects 详细使用说明

    Rollup 目前也已经支持 sideEffectissuees 讨论

2、纯注释 /*#__PURE__/

  • 如果我们确定这个函数不存在,我们可以通过 /*@__PURE__*/ 或者 /*#__PURE__/,通过注释的方式告诉编译器,这不存在副作用。

  • 有的时候,可能一些包里存在这些纯注释,但是确实对我们的业务代码有影响,我们可以在 Rollup 的配置文件中,配置 output-> treeshake.annotationsfalse 告诉编译器忽略掉这些纯注释

    annotations.gif

    纯注释来告诉编译器是没有副作用的起源可以看这个issues的讨论

    Rollup 关于纯注释的讨论issues

3、webpack2 tree shaking是通过 uglifyJS 来做的处理。而 webapck4 以后,将其内置,当我们的 mode = production 的时候。mode = development 的时候不会进行 Tree shaking,只是因为开发环境如果优化掉依赖会让问题变得难以调试!

  • babel 处理我们的依赖的时候,如下图 在线尝试

image.png 可以看到,如果我们经过 babel 转移,再经过构建工具打包,此时如果我们没有 external 引用的 npm 包,那么构建工具就会将其全部打包进我们的产物中。我们在上文也说了 Tree shaking 是基于 es规范而babel 默认生成 cjs规范。针对于这种情况的解决方案:
我们直接使用对应的文件

import Grid from 'react-bootstrap/lib/Grid';  
import Row from 'react-bootstrap/lib/Row';  
import Col from 'react-bootstrap/lib/Col';

类似的这种情况会经常发生在我们日常的构建中: 比如 TS、Babel、WebPack、Rpllup 混用的情况,因为他们都有自己的编译器,所以我们需要时刻留意交给Tree shaking时的代码。

小结:

  1. 开发 npm 包的时候,提供 es 规范的包,以便进行 Tree shaking。并要在 package.json 中设置 module 字段,告诉编译器我们对应 es 包的地址。
  2. package.json 中增加 sideEffect 字段,当我们确定某些文件一定存在副作用的时候。
  3. 当存在副作用的方法时,我们将可以使用纯注释 /*@__PURE__*/ 的方式显式告诉编译器,这个函数虽然有副作用,删除不会对代码有影响.

随着构建工具的日渐强大,上面的问题也逐渐被以更好的方式处理,但是如果我们能够了解 Tree shaking 的痛点,以及不能被算法优化的常见情况,可以使我们在遇到特殊情况的时候,有一个排查、解决问题的方向。

认知有限,如有错误的地方,欢迎指正,感谢支持~

文章引用

exploringjs.com/es6/ch_modu…

en.wikipedia.org/wiki/Dead-c…

webpack.js.org/guides/tree…