配置Tree Shaking来减少JavaScript的打包体积

译者按: 用 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。

问题在于对于 CommonJS,tree 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来替代 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

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

版权声明

转载时请注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2018/08/15/reduce-js-payload-with-tree-shaking/

您的用户遇到BUG了吗?

体验Demo 免费使用