先来看一下Tree-shaking原始的本意javascript
上图形象的解释了Tree-shaking 的本意,本文所说的前端中的tree-shaking能够理解为经过工具"摇"咱们的JS文件,将其中用不到的代码"摇"掉,是一个性能优化的范畴。具体来讲,在 webpack 项目中,有一个入口文件,至关于一棵树的主干,入口文件有不少依赖的模块,至关于树枝。实际状况中,虽然依赖了某个模块,但其实只使用其中的某些功能。经过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。前端
Tree-shaking 较早由 Rich_Harris 的 rollup 实现,后来,webpack2 也增长了tree-shaking 的功能。其实在更早,google closure compiler 也作过相似的事情。三个工具的效果和使用各不相同,使用方法能够经过官网文档去了解,三者的效果对比,后文会详细介绍。java
Tree-shaking的本质是消除无用的js代码。无用代码消除在普遍存在于传统的编程语言编译器中,编译器能够判断出某些代码根本不影响输出,而后消除这些代码,这个称之为DCE(dead code elimination)。node
Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不一样的是,javascript绝大多数状况须要经过网络进行加载,而后执行,加载的文件大小越小,总体执行时间更短,因此去除无用代码以减小文件体积,对javascript来讲更有意义。webpack
Tree-shaking 和传统的 DCE的方法又不太同样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注宇消除没有用到的代码。下面详细介绍一下DCE和Tree-shaking。git
(1)先来看一下DCE消除大法github
Dead Code 通常具备如下几个特征web
•代码不会被执行,不可到达编程
•代码执行的结果不会被用到浏览器
•代码只会影响死变量(只写不读)
下面红框标示的代码就属于死码,知足以上特征
传统编译型的语言中,都是由编译器将Dead Code从AST(抽象语法树)中删除,那javascript中是由谁作DCE呢?
首先确定不是浏览器作DCE,由于当咱们的代码送到浏览器,那还谈什么消除没法执行的代码来优化呢,因此确定是送到浏览器以前的步骤进行优化。
其实也不是上面提到的三个工具,rollup,webpack,cc作的,而是著名的代码压缩优化工具uglify,uglify完成了javascript的DCE,下面经过一个实验来验证一下。
如下全部的示例代码都能在咱们的github中找到,欢迎戳❤
分别用rollup和webpack将图4中的代码进行打包
中间是rollup打包的结果,右边是webpack打包的结果
能够发现,rollup将无用的代码foo函数和unused函数消除了,可是仍然保留了不会执行到的代码,而webpack完整的保留了全部的无用代码和不会执行到的代码。
分别用rollup + uglify和 webpack + uglify 将图4中的代码进行打包
中间是配置文件,右侧是结果
能够看到右侧最终打包结果中都去除了没法执行到的代码,结果符合咱们的预期。
(2) 再来看一下Tree-shaking消除大法
前面提到了tree-shaking更关注于无用模块的消除,消除那些引用了但并无被使用的模块。
先思考一个问题,为何tree-shaking是最近几年流行起来了?而前端模块化概念已经有不少年历史了,其实tree-shaking的消除原理是依赖于ES6的模块特性。
ES6 module 特色:
ES6模块依赖关系是肯定的,和运行时的状态无关,能够进行可靠的静态分析,这就是tree-shaking的基础。
所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6以前的模块化,好比咱们能够动态require一个模块,只有执行后才知道引用的什么模块,这个就不能经过静态分析去作优化。
这是 ES6 modules 在设计时的一个重要考量,也是为何没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为何 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。
咱们仍是经过例子来详细了解一下
面向过程编程函数和面向对象编程是javascript最经常使用的编程模式和代码组织方式,从这两个方面来实验:
先看下函数消除实验
utils中get方法没有被使用到,咱们指望的是get方法最终被消除。
注意,uglify目前不会跨文件去作DCE,因此上面这种状况,uglify是不能优化的。
先看看rollup的打包结果
彻底符合预期,最终结果中没有get方法
再看看webpack的结果
也符合预期,最终结果中没有get方法
能够看到rollup打包的结果比webpack更优化
函数消除实验中,rollup和webpack都经过,符合预期
再来看下类消除实验
增长了对menu.js的引用,但其实代码中并无用到menu的任何方法和变量,因此咱们的指望是,最终代码中menu.js里的内容被消除
rollup打包结果
包中居然包含了menu.js的所有代码
webpack打包结果
包中居然也包含了menu.js的所有代码
类消除实验中,rollup,webpack 全军覆没,都没有达到预期
这跟咱们想象的彻底不同啊?为何呢?无用的类不能消除,这还能叫作tree-shaking吗?我当时一度怀疑本身的demo有问题,后来各类网上搜索,才明白demo没有错。
下面摘取了rollup核心贡献者的的一些回答:
再举个例子说明下为何不能消除menu.js,好比下面这个场景
function Menu() {
}
Menu.prototype.show = function() {
}
Array.prototype.unique = function() {
// 将 array 中的重复元素去除
}
export default Menu;
复制代码
若是删除里menu.js,那对Array的扩展也会被删除,就会影响功能。那也许你会问,难道rollup,webpack不能区分是定义Menu的proptotype 仍是定义Array的proptotype吗?固然若是代码写成上面这种形式是能够区分的,若是我写成这样呢?
function Menu() {
}
Menu.prototype.show = function() {
}
var a = 'Arr' + 'ay'
var b
if(a == 'Array') {
b = Array
} else {
b = Menu
}
b.prototype.unique = function() {
// 将 array 中的重复元素去除
}
export default Menu;
复制代码
这种代码,静态分析是分析不了的,就算能静态分析代码,想要正确彻底的分析也比较困难。
更多关于反作用的讨论,能够看这个
Tree shaking class methods · Issue #349 · rollup/rollupgithub.comtree-shaking对函数效果较好
函数的反作用相对较少,顶层函数相对来讲更容易分析,加上babel默认都是"use strict"严格模式,减小顶层函数的动态访问的方式,也更容易分析
咱们开始说的三个工具,rollup和webpack表现不理想,那closure compiler又如何呢?
将示例中的代码用cc打包后获得的结果以下:
天啊,这不就是咱们要的结果吗?完美消除全部无用代码的结果,输出的结果很是性感
closure compiler, tree-shaking的结果完美!
但是不能高兴得太早,能获得这么完美结果是须要条件的,那就是cc的侵入式约束规范。必须在代码里添加这样的代码,看红线框标示的
google定义一整套注解规范Annotating JavaScript for the Closure Compiler,想更多了解的,能够去看下官网。
侵入式这个就让人很不爽,google Closure Compiler是java写的,和咱们基于node的各类构建库不可能兼容(不过目前好像已经有nodejs版 Closure Compiler),Closure Compiler使用起来也比较麻烦,因此虽然效果很赞,但比较难以应用到项目中,迁移成本较大。
说了这么多,总结一下:
三大工具的tree-shaking对于无用代码,无用模块的消除,都是有限的,有条件的。closure compiler是最好的,但与咱们平常的基于node的开发流很难兼容。
tree-shaking对web意义重大,是一个极致优化的理想世界,是前端进化的又一个终极理想。
理想是美好的,但目前还处在发展阶段,还比较困难,有各个方面的,甚至有目前看来没法解
决的问题,但仍是应该相信新技术能带来更好的前端世界。
优化是一种态度,不因小而不为,不因艰而不攻。
知识有限,若是错误,请不惜指正,谢谢
下一篇将继续介绍 Tree-Shaking性能优化实践 - 实践篇
本文中示例代码都能在咱们的github中找到,欢迎戳❤