什么是Tree-shaking

所谓Tree-shaking就是‘摇’的意思,作用是把项目中没必要的模块全部抖掉,用于在不同的模块之间消除无用的代码,可列为性能优化的范畴。

Tree-shaking早期由rollup实现,后来webpack2也实现了Tree-shaking的功能,但是至今还不是很完备。至于为什么不完备,可以看一下百度外卖的Tree-shaking原理

Tree-shading原理

Tree-shaking的本质用于消除项目一些不必要的代码。早在编译原理中就有提到DCE(dead code eliminnation),作用是消除不可能执行的代码,它的工作是使用编辑器判断出某些代码是不可能执行的,然后清除。

Tree-shaking同样的也是消除项目中不必要的代码,但是和DCE又有略不相同。可以说是DCE的一种实现,它的主要工作是应用于模块间,在打包过程中抽出有用的部分,用于完成DCE。

Tree-shaking是依赖ES6模块静态分析的,ES6 module的特点如下:

  1. 只能作为模块顶层的语句出现
  2. import 的模块名只能是字符串常量
  3. import binding 是 immutable的

依赖关系确定,与运行时无关,静态分析。正式因为ES6 module的这些特点,才让Tree-shaking更加流行。

主要特点还是依赖于ES6的静态分析,在编译时确定模块。如果是require,在运行时确定模块,那么将无法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态。

Webpack4的Tree-shaking

webpack从第2版本就开始支持Tree-shaking的功能,但是至今也并不能实现的那么完美。凡是具有副作用的模块,webpack的Tree-shaking就歇菜了。

副作用

副作用在我们项目中,也同样是频繁的出现。知道函数式编程的朋友都会知道这个名词。所谓模块(这里模块可称为一个函数)具有副作用,就是说这个模块是不纯的。这里可以引入纯函数的概念。

对于相同的输入就有相同的输出,不依赖外部环境,也不改变外部环境。

符合上述就可以称为纯函数,不符合就是不纯的,是具有副作用的,是可能对外界造成影响的。

webpack自身的Tree-shaking不能分析副作用的模块。以lodash-es这个模块来举个例子

//test.js
import _ from "lodash-es";const func1 = function(value){return _.isArray(value);
}
const func2 = function(value){return value=null;
}export {func1,func2,
}
//index.js
import {func2} from './test.js'
func2()
复制代码

上述代码在test.js中引入lodash-es,在func1中使用了loadsh,并且这里不符合纯函数的概念,它是具有副作用的。func2是一个纯函数。

在index.js中只引入了func2,并且使用了func2,可见整个代码的执行是和func1是没有任何关系的。我们通过生产环境打包一下试试看(Tree-shaking只在生产环境生效)

main.js 91.7KB,可见这个结果是符合我们的预期的,因为func1函数的副作用,webpack自身的Tree-shaking并没有检测到这里有没必要的模块。解决办法还是用的,webpack的插件系统是很强大的。

webpack-deep-scope-plugin

webpack-deep-scope-plugin是一位中国同胞(学生)在Google夏令营,在导师Tobias带领下写的一个webpack插件。(此时慢慢的羡慕)

这个插件主要用于填充webpack自身Tree-shaking的不足,通过作用域分析来消除无用的代码。

插件的原理

这个插件是基于作用域分析的,那么都有什么样的作用域?

// module scope start// Block{ // <- scope start
} // <- scope end// Classclass Foo { // <- scope start} // <- scope end// If elseif (true) { // <- scope start} /* <- scope end */ else { // <- scope start} // <- scope end// Forfor (;;) { // <- scope start
} // <- scope end// Catchtry {} catch (e) { // <- scope start} // <- scope end// Functionfunction() { // <- scope start
} // <- scope end// Scopeswitch() { // <- scope start
} // <- scope end// module scope end
复制代码

对于ES6模块来说,上面作用域只有function和class是可以被导出的,其他的作用域可以称之为function和class的子作用域并不能被导出实际上归属于父作用域的。

插件通过分析代码的作用域,进而得到作用域与作用域之间的关系。

分析作用域

分析代码的作用域的基础是建立做AST(Abstract Syntax Tree)抽象语法树上面的。这个可以通过escope来完成。

拿到解析完的AST抽象语法树,利用图的深度优先遍历找到哪些作用域是可以被使用到的,哪些作用域是不可以被使用到的。从而分析作用域之间的关系和导出变量之间的关系。进而执行模块消除。

插件的不足

JavaScript中还是有一些代码是不会消去的。

根作用域的引用
import { isNull } from 'lodash-es';export function scope(...args) {return isNull(...args);
}复制代码

在根作用域引用到的作用域不会被消除。

给变量重新赋值
import _ from "lodash-es";var func1
func1 = function(value){return _.isArray(value);
}
const func2 = function(value){return value=null;
}export {func1,func2,
}
复制代码

上述代码中先定义了func1,然后又给func1赋值,这样缺少了数据流分析,同样插件也是不可以的。

纯函数调用

引用作者的例子

import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';var allPass = /*#__PURE__*/_curry1(function allPass(preds) {return curryN(reduce(max, 0, pluck('length', preds)), function () {var idx = 0;var len = preds.length;while (idx < len) {if (!preds[idx].apply(this, arguments)) {return false;}idx += 1;}return true;});
});
export default allPass;
复制代码

当一个匿名函数被包在一个函数调用中(IIFE也是如此),那么插件是无法分析的。但是如果加上/*#__PURE__*/注释的话,这个插件会把这个函数调用当作一个独立的域,tree-shaking是可以生效的。

探讨的一些问题

我们都知道在这个ES6泛滥的时代,ES6的代码在项目中出现已经很广泛。(先不考虑线上环境打包成ES5)。上面提到插件的利用作用域来分析。能导出的作用域只有class和funciton。function的情况在上面已经说过,现在来探讨一下class的情况。

no plugin

当不使用插件的时候,我们来看一下会不会Tree-shaking,预期是会被Tree-shaking。书写下面这样一段简单的代码。

class Test{init(value){console.log('test init');}
}
export {Test,
}
复制代码

我们发现在没有使用插件的情况下,被Tree-shaking了。预期相同。

no plugin + 副作用

当我们在不适用插件的情况下,并且引入副作用,观察一下会不会打包,预期是不会打包。书写下面代码。

class Test{init(value){console.log('test init');return _.isArray(value);}
}
export {Test,
}
复制代码

观察打包结果,main.js 91.7KB,并没有被Tree-shaking,符合预期的结果。

plugin + 副作用

当我们使用插件并且代码中存在副作用的情况下,观察打包情况。由于上面的插件原理的铺垫,我们预期这次是可以Tree-shaking的。利用上例代码来测试。

我们观察可以看出main.js 6.78KB,Tree-shaking生效。

plugin + 副作用 + babel

由于用户浏览器对ES6支持度不够的原因,线上的代码不能全是ES6的,有时候我们要把ES6的代码打包成ES5的,放到线上环境来执行。利用上例代码来测试。

??? 什么鬼,我没有用到它,为什么这么大??? 一串懵逼

成也babel,败也babel

懵逼懵逼,babel成就了线上生产环境,但失去了Tree-shaking优化。我们来看看怎么回事。

没有副作用的情况

当去除调副作用的时候我们来打包一下。

没有找到test init Tree-shaking生效。为什么呢?我们使用babel线上工具编译一下源代码。

"use strict";function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return right[Symbol.hasInstance](left); } else {return left instanceof right; } }function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); }
}function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); }
}function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }var Test =/*#__PURE__*/function () {function Test() {_classCallCheck(this, Test);}_createClass(Test, [{key: "init",value: function init(value) {console.log("test init")}}]);return Test;}();
复制代码

上面可以看到最新的babel和webpack有了契合,在Test立即执行函数的地方使用了 /*#__PURE__*/(忘记可以往上看),让下面的IIFE变成可分析的,成功了使用了Tree-shaking。

有副作用的情况下

上面探讨情况的时候就得知有副作用的情况下,不可以被打包的。ES6编译代码如下。

"use strict";function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {return right[Symbol.hasInstance](left); } else {return left instanceof right; } }function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); }
}function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); }
}function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }var Test =/*#__PURE__*/function () {function Test() {_classCallCheck(this, Test);}_createClass(Test, [{key: "init",value: function init(value) {console.log("test init")return _.isArray(value);}}]);return Test;}();
复制代码

这里虽然bable新版契合了webpack,但是还是有一些问题。自己也没有找出是哪里除了问题,作者说JavaScript代码还是有一些是不可以清除的,也许就出现到这里。提供一个作者的插件Demo。

babel的解决方案

无论是ES6,还是ES5,Tree-shaking不能生效的原因总的归根结底还是因为代码副作用的问题。可想而知代码的书写规范是多么重要。这里我所想出的解决方案有两种。

1.代码的书写规范,尽量避免副作用。

书写代码过程中尽量使用纯函数的方式来写代码,保持书写规范,不让代码有副作用。例如把class类引用的副作用改成纯的。

class Test{init(value,_){  //参数引入lodash模块console.log('test init');return _.isArray(value);}
}
export{Test,
}
复制代码

可以看出,没有副作用的ES6代码编译成ES5,Tree-shaking也是生效的。

2.灵活使用ES6代码

两套代码。当浏览器支持的时候,就使用ES6的代码,ES5的代码。此方案可参考浏览器支持ES6的最优解决方案

总结

项目中难免会一些用不到的模块占位置影响我们的项目,Tree-shaking的出现也为开发者在性能优化方面提供了非常大的帮助,灵活使用Tree-shaking才能让Tree-shaking发挥作用,处理好项目中代码的副作用可以使项目更加的完美。

引用文章

webpack 如何通过作用域分析消除无用代码

Tree-Shaking性能优化实践 - 原理篇

原文发布于Webpack4:Tree-shaking深度解析

Webpack4: Tree-shaking 深度解析相关推荐

  1. Webpack 实现 Tree shaking 的前世今生

    作者 | 左琳       责编 | 欧阳姝黎 前言 如果看过 rollup 系列的这篇文章 - 无用代码去哪了?项目减重之 rollup 的 Tree-shaking,那你一定对 tree-shak ...

  2. tree shaking的原理是什么?

    什么是tree-shaking 别名叫摇晃树,最早是由Rollup实现,是一种采用删除不需要的额外代码的方式优化代码体积的技术 tree-shaking会在打包过程中 静态分析 模块之间的导入导出,确 ...

  3. 前端基础建设与架构11 Tree Shaking:移除 JavaScript 上下文中的未引用代码

    时至今日,Tree Shaking 对于前端工程师来说,已经不是一个陌生的名词了.顾名思义:Tree Shaking 译为"摇树",它通常用于描述移除 JavaScript 上下文 ...

  4. 单文件浏览器_图文并茂深度解析浏览器渲染原理,包看懂超值得收藏

    在我们面试过程中,面试官经常会问到这么一个问题,那就是从在浏览器地址栏中输入URL到页面显示,浏览器到底发生了什么?这个问题看起来是老生常谈,但是这个问题回答的好坏,确实可以很好的反映出面试者知识的广 ...

  5. 深度解析Cocoa异步请求和libxml2.dylib教程(1)

    深度解析Cocoa异步请求和libxml2.dylib教程是本文要介绍的内容,不多说,直接进入话题,很早就在cocoachina上看到这个框架了,今天终于有机会来使用这个东东了. 我这里写一下,如何往 ...

  6. webpack学习(四) -- css tree shaking

    css tree shaking 我们和webpack(三)一样,用案例说话 安装css需要的两个loader,css-loader 和style-loader npm install css-loa ...

  7. demo10 关于JS Tree Shaking

    1.什么是 tree shaking 借助于 es6 (es2015) 模块系统 (import 和 export) 的静态解析,webpack 能够利用 Tree Shaking 进行按需加载,移除 ...

  8. ConcurrentHashMap源码深度解析(一)(java8)——不可不知的基本概念(助你拿下源码事半功倍)

    文章目录 一.前言 二.基本定义 1.基本常量 2.基本属性 三.构造器优化 1.ConcurrentHashMap(int initialCapacity, float loadFactor, in ...

  9. webpack5的tree shaking值得了解

    什么是 tree shaking?tree shaking 的意思是,webpack 在打包的时候将会剔除掉被没有被使用到的代码达到减小报体积,缩短 http 请求时间,起到一定效果的页面优化. 那么 ...

最新文章

  1. Open3d学习计划—高级篇 5(RGBD融合)
  2. 手抖有救了!DeblurGAN消除运动模糊效果惊人 | 附论文+代码
  3. 大多数的自动驾驶公司,注定要倒闭
  4. 【嵌入式】C语言高级编程-长度为0的数组(05)
  5. 用pycharm写python_如何利用pyCharm编写和运行python文件
  6. kafka----kafka connect的使用(一)
  7. 【1】Matlab深度学习环境配置-入门
  8. Layui表单自定义验证规则
  9. 沐川中学2021级高考成绩查询,沐川县中学2021年排名
  10. 阿里云服务器租用价格表分享(北京地域、入门级、windows系统)
  11. 读书百客:《临江仙引·画舸》赏析
  12. 讯飞语音的使用及常遇到的问题
  13. 自定义8583模板,打包解包,使用j8583包有改动
  14. 记录使用scrapy爬取新闻网站最新新闻存入MySQL数据库,每天定时爬取自动更新
  15. 鸿蒙手机什么时候可以上线?在你有钱的时候!
  16. win7睡眠功能无法使用(睡眠按钮变灰)
  17. W ndows主进程已停止,windows主进程rundll32已停止工作的四种解决方法介绍
  18. [转] meta标签的使用方法
  19. WiFi基础学习到实战(二:WiFi网络“数据链路层”)
  20. 从阿里安全峰会看它如何为安全奠基石

热门文章

  1. layui数据表格获取当前页数
  2. 威海二职工业机器人专业_工业机器人专业就业前景-山东省好的中专学校
  3. Toad for Oracle 导入MIP.dmp文件时:报内存不足时的解决办法:
  4. edge如何导入html文件收藏夹,edge浏览器收藏夹如何导入?edge浏览器收藏夹导入方法...
  5. leetcode1528. 重新排列字符串
  6. leetcode105 前序中序遍历序列构造二叉树
  7. 算法(7)-leetcode-explore-learn-数据结构-数组-小结
  8. Java中int[]与Integer[]相互转化的方法
  9. 3分钟入门python_3分钟带你了解世界第一语言Python 入门上手也这么简单!
  10. STL源码剖析 算法章节 算法总览