译者按 用Tree Shaking技术来减少JavaScript的Payload大小
为了保证可读性本文采用意译而非直译。另外本文版权归原作者所有翻译仅用于学习。
小编推荐Fundebug专注于JavaScript、微信小程序、微信小游戏Node.js和Java线上bug实时监控。真的是一个很好用的bug监控服务众多大佬公司都在使用。
如今一个网页应用可以体积很大特别是JavaScript代码。2018年年中HTTP Archive统计在移动端JavaScript文件的平均传输大小将近350KB。你要知道这仅仅是传输的大小。在网络传输的时候JavaScript往往是经过压缩的。也就是说在浏览器解压缩之后实际的大小会远远大于这个值。而这一点相当重要。如果考虑到浏览器处理数据的资源消耗其中压缩是不得不考虑的。一个300KB的文件解压缩会达到900KB并且在分析和编译的时候体积依然是900KB。
其实处理JavaScript是很耗资源的。不像图片只会在下载的时候有一点简单的解码处理JavaScript需要分析编译然后再被执行。一个字节一个字节地处理所以JavaScript的处理很贵。
为了优化JavaScript引擎各种改进方法被提出来。提升JavaScript代码的性能是开发者最擅长的事情。毕竟有谁比架构师更擅长优化架构的性能呢
Code splitting是其中一个用来提升性能的方法通过将JavaScript应用拆分成一个个块然后在需要的时候才下载。这个方法很好但是有一个很常见的问题没有处理那就是有很多打包的代码我们压根没有用到。为了解决这个问题我们用tree shaking。
什么叫tree shaking
Tree shaking是一种消除无用代码(dead code)的方式。这个词是由最先从Rollup社区开始流行的不过本身的理念很早就有了。在webpack中也有相同的理念在本文我们会用一个例子来描述。
“tree shaking”这个词来自于应用的架构以及本身的依赖关系就像一个树形结构。树的每一个节点表示应用中一个唯一的功能。在现代网页应用中依赖关系通常使用static import statement如下所示
// Import all the array utilities! import arrayUtils from "array-utils"; |
注意如果你不了解ES6我强烈推荐你阅读Pony Foo上面的这篇文章。我们这篇文章假定你对ES6有一定的了解。如果没有赶紧学学去吧。
当你的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将ES6编译到CommonJS
Babel在很多应用中已经必不可少。不幸的是它会让tree shaking变得困难。如果你使用babel-preset-env它会将你的ES6编译到可兼容性更好的CommonJS。
问题在于对于CommonJStree shaking非常困难而且webpack不知道哪些需要消除掉。不过呢好在有一个很简单的解法配置babel-preset-env
让其保持ES6不动不要翻译。具体的配置放在你配置Babel的地方(.babelrc
或则package.json
)
{ "presets": [ ["env", { "modules": false }] ] } |
简单地配置"modules":false
即可webpack会分析所有文件中模块的依赖关系然后剔除那些没有使用的代码。并且这个处理不会有兼容问题因为webpack最终会将代码转换到兼容的版本。
谨记副作用(Side Effect)
另一个需要考虑的是应用中使用模块是否有副作用。我举一个例子来说什么叫副作用(这个例子表述了在一个函数中去修改函数外部的变量):
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); } |
接下来我们看看执行效果首先回顾之前的打包效果
接下来看使用了tree shaking后的效果
两个模块都变小了特别是main文件。通过将utils中无用代码删掉整个体积削减了60%。这不仅节省了下载时间而且节省了处理时间。
其他情况
在大多数情况下上面的方法就足够了。但是总有例外的情况会让你抓耳挠腮。比如Lodash就不行。因为Lodash当时的架构就不支持所以需要一些额外的工作a) 安装lodash-es来替代lodashb) 使用稍微不同的语法(叫做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才是你最佳的选择。
原文链接https://blog.fundebug.com/2018/08/15/reduce-js-payload-with-tree-shaking/