前言:最近在团队中负责公共基础服务的建设,封装了公共类库及组件库,以为能带来比较大的便捷,真是理想很丰满,现实很骨感,开发时通过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 | // 修改后的tsconfig.json |
修改完 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 | var V8Engine = (function () { |
V6Engine
虽然没有被使用,但是它修改了V8Engine原型链上的属性,这就产生副作用了。如果 V6Engine
这个IIFE里面再搞一些全局变量的声明,那就当然不能删除了。那就没有解决方案吗?在你的Tree-Shaking并没什么卵用中最后有提供解决方案。
如果想利用好 Webpack 的 Tree Shaking,我们需要规范自己的代码:
- 使用 ES2015 模块语法(即
import
和export
) - 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)
- 尽量不写带有副作用的代码。诸如编写了立即执行函数,在函数里又使用了外部变量等
- 如果JavaScript库开发中,难以避免的产生各种副作用代码,可以将功能函数或者组件,打包成单独的文件或目录,以便于用户可以通过目录去加载。如有条件,也可为自己的库开发单独的webpack-loader,便于用户按需加载
- 配置TypeScript编译器以
"esnext"
用作module
,使代码能以ES module
导出
目前也是越来越多的 Npm 第三方模块考虑到了 Tree Shaking,并对其提供了支持。 相信 Tree Shaking 也会越来越成熟。