导航
导航
文章目录
  1. 什么是Tree Shaking
  2. Tree Shaking的原理
    1. 静态分析能力
    2. dead-code
  3. TS 编译的类库
  4. Webpack Tree Shaking
  5. 参考

Tree Shaking知多少

前言:最近在团队中负责公共基础服务的建设,封装了公共类库及组件库,以为能带来比较大的便捷,真是理想很丰满,现实很骨感,开发时通过ES Module引入及使用都很方便,的确达到了开发效率的提升,但后续打包发布测试时却遇到了难题,目标项目中未使用的代码也一并打包进来了,导致最后的bundle过大。那问题就很明显了,需要去除掉未使用的代码。

下面跟分享下,我在Tree Shaking上的摸索历程。

什么是Tree Shaking

用个简单的栗子描述一下:我们将应用程序想象成一棵树。绿色叶子表示实际用到的 source code(源码) 和 library(库),是树上活的树叶。灰色叶子表示未引用代码,是树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

具体来说,在 webpack 项目中,有一个入口文件,相当于一棵树,入口文件有很多依赖的模块,相当于树叶。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 Tree Shaking,将没有使用的模块摇掉,这样来达到删除无用代码(dead-code)的目的。

Tree Shaking 较早由 rollup 提出并实现,后来,webpack 从 webpack2 开始也增加了 Tree Shaking 的功能。

Tree Shaking的原理

Tree Shaking 本质是借助 ES module 的静态分析能力来消除无用的代码(dead-code)。

静态分析能力

Tree Shaking 的目的是减少文件的体积,节约带宽,提高加载速度,所以文件必须在浏览器加载之前完成瘦身,也就是在打包的时候完成这个功能。ES6 使用 import 和 export 可以在编译期确定模块间的依赖关系,这是 Tree Shaking 的必需条件,这也意味着被 Babel 编译成 ES5 的代码是不能 Tree Shaking 的。

dead-code

我们需要理解什么是无用的代码,满足以下特征,即是无用代码:

  • 代码不会被执行,不可达到
  • 代码执行的结果不会被用到
  • 代码只会影响死变量(只写不读)

Tree Shaking的原理总结下来就是以下两点:

1、ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码
2、分析程序流,判断哪些变量未被使用、引用,进而删除此代码

TS 编译的类库

原理理解后,那对于开头那个问题就比较好解决了。

查看我的tsconfig.json后果然配置有误,配置的 "target": "ES5" 没有配置 module 导致 TS 编译成 CommonJS 模块了,也就无法满足 Tree Shaking 的条件。

1
2
3
4
5
6
7
8
9
// 修改后的tsconfig.json
{
...
"compilerOptions": {
"target": "ES5",
"module": "ESNext"
...
}
}

修改完 TS 编译配置后,让目标项目重新打包后就符合预期,去除了无用的代码,bundle的体积也明显的减少了。

到这里,感觉万事大吉了,然而比较坑的是用 class 写的类库还是没办法消除,还是会被打包进去。

还是我们上面配置的"target": "ES5" 的问题,因为我们项目还需要兼容IE(万恶的IE),所以需要配置编译为es5,导致我们的 class 被编译成了 IIFE (立即调用函数表达式),又有一个新的问题:Webpack Tree Shaking不会清除IIFE

Webpack Tree Shaking

当我们用 Webpack 配合 UglifyJS 打包文件时,我们class类的IIFE又被打包进去了。这跟我们想象的完全不一样啊?为什么呢?无用的类不能消除,这还能叫做 Tree Shaking 吗。

你的Tree-Shaking并没什么卵用中有过分析,里面有一个例子比较好,见下文

原因很简单:uglify没有完善的程序流分析。它可以简单的判断变量后续是否被引用、修改,但是不能判断一个变量完整的修改过程,不知道它是否已经指向了外部变量,所以很多有可能会产生副作用的代码,都只能保守的不删除

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
var V8Engine = (function () {
function V8Engine () {}
V8Engine.prototype.toString = function () { return 'V8' }
return V8Engine
}())
var V6Engine = (function () {
function V6Engine () {}
V6Engine.prototype = V8Engine.prototype // <---- side effect
V6Engine.prototype.toString = function () { return 'V6' }
return V6Engine
}())
console.log(new V8Engine().toString()) // V6

V6Engine虽然没有被使用,但是它修改了V8Engine原型链上的属性,这就产生副作用了。如果 V6Engine 这个IIFE里面再搞一些全局变量的声明,那就当然不能删除了。那就没有解决方案吗?在你的Tree-Shaking并没什么卵用中最后有提供解决方案。

如果想利用好 Webpack 的 Tree Shaking,我们需要规范自己的代码:

  • 使用 ES2015 模块语法(即 importexport
  • 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档
  • 尽量不写带有副作用的代码。诸如编写了立即执行函数,在函数里又使用了外部变量等
  • 如果JavaScript库开发中,难以避免的产生各种副作用代码,可以将功能函数或者组件,打包成单独的文件或目录,以便于用户可以通过目录去加载。如有条件,也可为自己的库开发单独的webpack-loader,便于用户按需加载
  • 配置TypeScript编译器以"esnext"用作 module,使代码能以 ES module 导出

目前也是越来越多的 Npm 第三方模块考虑到了 Tree Shaking,并对其提供了支持。 相信 Tree Shaking 也会越来越成熟。

参考