tree-shaking不完全指南

什么是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%7CimageView2/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%7CimageView2/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是可以分析出来的,而有的,是难以解析的。

参考链接

1. CommonJS
2. tree-shaking versus dead code elimination
3. ecma-262 sec-modules

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容