什么是 tree-shaking 以及 Tree-shaking 的前置依赖
关于什么是 tree-shaking 可以看 这篇文章 有一个简单介绍.
tree-shaking 的目的
简单来说,为了增强用户体验,用户打开页面所需等待的时间是非常重要的一环.而在用户打开页面所需等待的时间,有一部分时间就是用来加载远程文件,包括 html,JavaScript,CSS 以及图片资源等文件.
taobao
如图就是淘宝页面在初始加载时所加载的资源,此处只截取部分.
因此,tree-shaking 的目的,就是通过减少 web 项目中 JavaScript 的无用代码,以达到减少用户打开页面所需的等待时间,来增强用户体验.对于消除无用代码,并不是 JavaScript 专利,事实上业界对于该项操作有一个名字,叫做 DCE(dead code elemination),然而与其说 tree-shaking 是 DCE 的一种实现,不如说 tree-shaking 从另外一个思路达到了 DCE 的目的.
tree-shaking 与 dead code elemination
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.
You'd probably eat less cake, for one thing.
That's what dead code elimination consists of - taking the finished product, and imperfectly removing bits you don't want. Tree-shaking, on the other hand, asks the opposite question: given that I want to make a cake, which bits of what ingredients do I need to include in the mixing bowl
关于 tree-shaking 与 DCE 的区别,rollup 的主要贡献者 Rich Harris 用做蛋糕这样一个例子来进行对比,假设我们需要用鸡蛋这个原材料来做蛋糕,很显然,我们要的只是鸡蛋里的蛋清或者蛋黄而不是蛋壳,关于如何去除蛋壳,DCE 是这样做的:直接把整个鸡蛋放到碗里搅拌做蛋糕,蛋糕做完后再慢慢的从里面挑出蛋壳;相反 tree-shaking 在开始阶段,就不会把蛋壳放进碗里,而是拿出蛋清和蛋黄放进碗里搅拌,蛋壳呢?蛋壳在一开始就已经丢进垃圾桶里了.
实现 tree-shaking 的前提条件
首先既然要实现的是减少浏览器下载的资源大小,因此要 tree-shaking 的环境必然不能是浏览器,一般宿主环境是 Node.
其次如果 JavaScript 是模块化的,那么必须遵从的是 ES6 Module 规范,而不是 CommonJS(由于 CommonJS 规范所致)或者其他,这是因为 ES6 Module 是可以静态分析的,故而可以实现静态时编译进行 tree-shaking.为什么说是可以静态分析的,是因为 ES6 制定了以下规范:
Module Syntax
Module :
ModuleBody
ModuleBody :
ModuleItemList
ModuleItemList :
ModuleItem
ModuleItemList ModuleItem
ModuleItem :
ImportDeclaration
ExportDeclaration
StatementListItem
上述语法摘自 ECMAScript 2015 spec.
关于 ES6 模块该写什么不该写什么,ecma-262 规范上已经说的很清楚了,ModuleItem 里只能包含 ImportDeclaration,ExportDeclaration 以及 StatementListItem,而关于 StatemengListItem,规范里又有如下说明:
## Block Syntax
BlockStatement[Yield, Return] :
Block[?Yield, ?Return]
Block[Yield, Return] :
{ StatementList[?Yield, ?Return]opt }
StatementList[Yield, Return] :
StatementListItem[?Yield, ?Return]
StatementList[?Yield, ?Return] StatementListItem[?Yield, ?Return]
StatementListItem[Yield, Return] :
Statement[?Yield, ?Return]
Declaration[?Yield]
刚才说到,一个模块只能包含 StatementListItem,ImportDeclaration,ExportDeclaration,而 StatementListItem 中又不能包含 ImportDeclaration,ExportDeclaration.这也就是说 import 和 export 语句只能出现在代码顶层,像如下代码是不符合 ES6 Modules 规范的:
if(a === true){
import func from './func'
}![48041852.png](http://upload-images.jianshu.io/upload_images/656716-e1ec93b7568093ad.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)
这样做的目的就是避免让模块分析依赖代码运行,从而促使 Modlus 可以进行静态解析.
tree-shaking 的实践分析
关于 tree-shaking 的实践分析,有 一篇文章 介绍的非常好,其从 webpack 和 rollup 两个主要的打包工具进行分析,描述了两者之间的异同及局限性.下面就对其进行一个简单的概括和整理.
rollup 与 webpack 的差异
1. 对于单个文件来说,rollup 不需要配置插件就可以进行 tree-shaking,而 webpack 要实现 tree-shaking 必须依赖 uglifyJs
single file
左边是原始代码,可以看出该代码真正执行的只有 app,函数 b 并未执行.中间是 rollup 的打包结果,可以发现 rollup 的 tree-shaking 是符合预期的;右侧 webpack 代码中,app 函数和未使用的 b 函数均被打进 webpack.bundle.js 文件中.
如果 webpack 配合 uglifyjs 插件,结果如下:
webpack with uglify![47827657.png](http://upload-images.jianshu.io/upload_images/656716-8972d40ab305224b.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)
可以看到成功移除了无用的 b 函数.
2. 对于模块化来说,rollup 依然可以不依赖其他插件实现 tree-shaking,webpack 依然依赖 uglifyJs.
module
可以发现,webpack 仅仅是通过注释来标识,该模块未使用,要想真正移除,还需要依赖 uglifyJs.
webpack module
假如 uglifyJs 后成功移除.
局限性
难道 tree-shaking 真正那么完美吗,并不是,下面就来谈谈局限性.
1. 对于未执行到的代码,单独使用 rollup 并不能移除,依然需要依赖 uglifyJs
unused
上面是未使用 uglifyJs 的打包结果.
rollup-uglify
可以发现,通过 uglifyJs 的配合,rollup 成功移除了函数中未执行的代码.
2. 对于依赖运行时才能确定是否会使用代码,tree-shaking 无法删除
关于 tree-shaking 的局限性,这里有篇文章 你的 Tree-Shaking 并没什么卵用 ,说的不错,但是其有部分内容,在我看来是有一定歧义的.
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _createClass = function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
"value" in descriptor && (descriptor.writable = !0), Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function(Constructor, protoProps, staticProps) {
return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
Constructor;
};
}()
var Person = function () {
function Person(_ref) {
var name = _ref.name, age = _ref.age, sex = _ref.sex;
_classCallCheck(this, Person);
this.className = 'Person';
this.name = name;
this.age = age;
this.sex = sex;
}
_createClass(Person, [{
key: 'getName',
value: function getName() {
return this.name;
}
}]);
return Person;
}();
我们的 Person 类被封装成了一个 IIFE(立即执行函数),然后返回一个构造函数.那它怎么就产生副作用了呢?问题就出现在_createClass 这个方法上,你只要在上一个 rollup 的 repl 链接中,将 Person 的 IIFE 中的_createClass 调用删了,Person 类就会被移除了.
这篇文章以 Person 类为例,想说代码之所以无法 tree-shaking,是因为该代码里含有副作用所以无法移除,以至于你的 tree-shaking 毫无卵用.然而事实真的是这样吗?
image
我同样以 IIFE 为例,来说明.
这里可以看出来,在 IIFE 中,同样拥有含有副作用的代码,如果按照那篇文章所述,因为代码里有含有副作用的代码,那么即使 Person 没有被使用,其所有代码依然都会被打进去,导致 tree-shaking 无任何作用.
下面来看一下 rollup 的打包结果.
image
可以发现,tree-shaking 后的代码,只保留了有副作用的代码,对于其他无副作用的代码,均被删除.
该文章中 Person 之所以里面的代码没有被删除,作者的先放一边,让读者感觉似乎只要代码里有副作用,整个代码就无法 tree-shaking,其实并不是这样.我们更换代码的写法,会发现有不同的打包结果:
image
image
因此,同样的有副作用,有的代码 tree-shaking 是可以分析出来的,而有的,是难以解析的.
参考链接
. CommonJS
. tree-shaking versus dead code elimination
. ecma-262 sec-modules
来源: http://www.jianshu.com/p/199850576e8c