译者按: 用Tree Shaking技术来减小JavaScript的Payload大小javascript
为了保证可读性,本文采用意译而非直译。另外,本文版权归原做者全部,翻译仅用于学习。html
小编推荐:Fundebug专一于JavaScript、微信小程序、微信小游戏,Node.js和Java线上bug实时监控。真的是一个很好用的bug监控服务,众多大佬公司都在使用。java
现在一个网页应用能够体积很大,特别是JavaScript代码。2018年年中,HTTP Archive统计在移动端JavaScript文件的平均传输大小将近350KB。你要知道,这仅仅是传输的大小。在网络传输的时候,JavaScript每每是通过压缩的。也就是说,在浏览器解压缩以后,实际的大小会远远大于这个值。而这一点至关重要。若是考虑到浏览器处理数据的资源消耗,其中压缩是不得不考虑的。一个300KB的文件解压缩会达到900KB,而且在分析和编译的时候,体积依然是900KB。webpack
其实,处理JavaScript是很耗资源的。不像图片只会在下载的时候有一点简单的解码处理,JavaScript须要分析,编译,而后再被执行。一个字节一个字节地处理,因此JavaScript的处理很贵。git
为了优化JavaScript引擎,各类改进方法被提出来。提高JavaScript代码的性能,是开发者最擅长的事情。毕竟,有谁比架构师更擅长优化架构的性能呢?es6
Code splitting是其中一个用来提高性能的方法,经过将JavaScript应用拆分红一个个块,而后在须要的时候才下载。这个方法很好,可是有一个很常见的问题没有处理,那就是有不少打包的代码咱们压根没有用到。为了解决这个问题,咱们用tree shaking。github
Tree shaking是一种消除无用代码(dead code)的方式。这个词是由最早从Rollup社区开始流行的,不过自己的理念很早就有了。在webpack中也有相同的理念,在本文咱们会用一个例子来描述。web
"tree shaking"这个词来自于应用的架构以及自己的依赖关系就像一个树形结构。树的每个节点表示应用中一个惟一的功能。在现代网页应用中,依赖关系一般使用static import statement,以下所示:数据库
// Import all the array utilities! import arrayUtils from "array-utils"; 复制代码
注意:若是你不了解ES6,我强烈推荐你阅读Pony Foo上面的这篇文章。咱们这篇文章假定你对ES6有必定的了解。若是没有,赶忙学学去吧。npm
当你的app还很小的时候,也许只有不多的依赖文件。并且应该几乎使用了全部你本身添加的依赖。可是,当你的app开发了一段时间,愈来愈多的依赖添加进去。因为各类缘由,旧的依赖可能根本没有使用了,可是呢依然在你的代码库里面,没有被删除。最终致使你的app夹带了不少并没有使用的JavaScript。经过分析咱们如何使用import语句,tree shaking会移除无用代码。
// Import only some of the utilities! import { unique, implode, explode } from "array-utils"; 复制代码
这个import语句和以前的区别在于,与其引入整个array-utils,而整个array-utils可能有很是多的函数,不如只引入咱们须要的部分。在开发构建的时候,这两种使用方法并无区别。可是在生产打包的时候,咱们能够配置webpack来剔除不须要的函数,使得整个代码文件变小。在这篇文章中,咱们会指导你如何作。
为了演示起见,我写了一个简单的单页应用。你能够克隆代码并跟着操做。我会详细描述每一步,因此克隆不是必备步骤。
示例是一个能够搜索吉他效果器的数据库。
应用在构建的时候,全部的JavaScript文件打包成了一个vendor和一个app文件。
上图中的文件是打包后的结果,已经通过uglification。21.1KB的大小彻底能够接受。不过,当前是没有使用tree shaking来优化的结果。咱们来看看如何进一步优化。
在任何应用中,寻找使用tree shaking优化的机会首先要寻找import语句。通常都在component文件的顶部,像这样:
import * as utils from "../../utils/utils"; 复制代码
也许你已经看过这样的语句。其实ES6中有多种导入模块的方法,不过这样的导入语句最值得注意。由于它意味着导入utils模块中的全部函数,并放到utils的命名空间下面。全部,一个最大的疑问是:在模块中到底有多少函数?
若是你查看utils模块的源代码,你会发现真的不少。大概有1300行的代码量。
不过,别担忧。也许全部的函数都在当前文件中使用了,对吧?咱们真的须要全部的函数吗?咱们来检查一下,经过查找utils.
,看看有几处使用。结果呢:
好吧,总共只找到了3处。 咱们再看看具体是哪一个函数?若是咱们一个一个地查看,会发现其实只用了一个函数,就是utils.simpleSort
。
if (this.state.sortBy === "model") { // Simple sort gets used here... json = utils.simpleSort(json, "model", this.state.sortOrder); } else if (this.state.sortBy === "type") { // ..and here... json = utils.simpleSort(json, "type", this.state.sortOrder); } else { // ..and here. json = utils.simpleSort(json, "manufacturer", this.state.sortOrder); } 复制代码
也就是说,咱们引入了一个1300行的文件,结果只使用了其中一个函数。
固然,咱们要认可这个例子为了演示目的,可能有故意之嫌。不过,它表述了一个事实,那就是在不少真实的应用中,存在着像这样须要优化的地方。那么如何作呢?
Babel在不少应用中已经必不可少。不幸的是,它会让tree shaking变得困难。若是你使用babel-preset-env,它会将你的ES6编译到可兼容性更好的CommonJS。
问题在于对于CommonJS,tree shaking很是困难,并且webpack不知道哪些须要消除掉。不过呢,好在有一个很简单的解法:配置babel-preset-env
,让其保持ES6不动,不要翻译。具体的配置放在你配置Babel的地方(.babelrc
或则package.json
):
{ "presets": [ ["env", { "modules": false }] ] } 复制代码
简单地配置"modules":false
便可,webpack会分析全部文件中模块的依赖关系,而后剔除那些没有使用的代码。而且,这个处理不会有兼容问题,由于webpack最终会将代码转换到兼容的版本。
另外一个须要考虑的是:应用中使用模块是否有反作用。我举一个例子来讲什么叫反作用(这个例子表述了在一个函数中去修改函数外部的变量):
let fruits = ["apple", "orange", "pear"]; console.log(fruits); // (3) ["apple", "orange", "pear"] const addFruit = function(fruit) { fruits.push(fruit); }; addFruit("kiwi"); console.log(fruits); // (4) ["apple", "orange", "pear", "kiwi"] 复制代码
在这个例子中,addFruit
修改了fruit
数组,而fruit
数组是全局的。
只有当函数给定输入后,产生相应的输出,而不修改任何外部的东西,咱们才能够安全的作shaking操做。
因此,在webpack中,咱们能够经过配置"sideEffects":false
表示模块是安全的,没有反作用的。
{ "name": "webpack-tree-shaking-example", "version": "1.0.0", "sideEffects": false } 复制代码
或则,你能够告诉webpack哪些文件有反作用:
{ "name": "webpack-tree-shaking-example", "version": "1.0.0", "sideEffects": [ "./src/utils/utils.js" ] } 复制代码
在上面的配置中,webpack会假定其它文件都是无反作用的。若是你不想添加到package.json
文件中,你能够配置module.rules
。
咱们能够只导入咱们须要使用的函数,在示例中,我么只须要simpleSort
:
import { simpleSort } from "../../utils/utils"; 复制代码
使用上面的语法,咱们就只会将simpleSort函数导出,咱们只须要将utils.simpleSort
改成simpleSort
:
if (this.state.sortBy === "model") { json = simpleSort(json, "model", this.state.sortOrder); } else if (this.state.sortBy === "type") { json = simpleSort(json, "type", this.state.sortOrder); } else { json = simpleSort(json, "manufacturer", this.state.sortOrder); } 复制代码
接下来咱们看看执行效果,首先回顾以前的打包效果:
在大多数状况下,上面的方法就足够了。可是,总有例外的状况会让你抓耳挠腮。好比,Lodash就不行。由于Lodash当时的架构就不支持,因此须要一些额外的工做:a) 安装lodash-es来替代lodash;b) 使用稍微不一样的语法(叫作cherry-picking):
// This still pulls in all of lodash even if everything is configured right. import { sortBy } from "lodash"; // This will only pull in the sortBy routine. import sortBy from "lodash-es/sortBy"; 复制代码
若是你倾向于使用一致的import语法,你可使用标准的lodash包,而后安装babel-plugin-lodash
。
若是有些模块使用CommonJS格式(module.exports),那么webpack没法使用tree shaking。一些插件(webpack-common-shake)为CommonJS提供tree shaking。可是,由于有些CommonJS的模式是没法作tree shaking的。若是你想很保险地剔除掉没有使用的依赖,ES6才是你最佳的选择。
Fundebug专一于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了6亿+错误事件,获得了Google、360、金山软件等众多知名用户的承认。欢迎免费试用!