JavaScript性能优化之摇树

作者|Jeremy Wagner译者|薛命灯

现代 Web 应用程序可能会变得非常巨大,特别是它们的 JavaScript 部分。HTTP Archive 网站的数据显示,截至 2018 年中,传输到移动设备上的 JavaScript 文件中值大约为 350 KB。而这只是传输大小,JavaScript 在通过网络传输时通常会被压缩,也就是说,在浏览器端解压后,JavaScript 的实际数量会更多。

从资源处理方面来看,压缩并不会给资源处理带来任何好处,比如 900 KB 的 JavaScript 被压缩后可能只有 300 KB,但在解压后解析器和编译器仍然要处理 900 KB 的 JavaScript。

上图是下载和运行 JavaScript 的过程。请注意,即使压缩后的脚本为 300 KB,但在后面仍然要解析、编译和执行 900 KB 的 JavaScript。

处理 JavaScript 非常耗费资源。与图像不一样,图像在下载完之后只需要对其进行解码,而 JavaScript 必须被解析、编译和执行,因此处理 JavaScript 比处理其他类型的资源更昂贵。

上图显示了解析和编译 170 KB JavaScript 与解码等效大小 JPEG 图像的处理成本。

引擎开发者在不断努力提升 JavaScript 引擎的执行效率,但说到底,提升 JavaScript 代码的性能更多的是开发人员的责任。

有一些技术可以用于提升 JavaScript 的性能。代码拆分就是这样的一种技术,它将应用程序 JavaScript 划分为较小的块,并只向应用程序路由提供它们必需的块,以此来提升性能。这种方式是有效的,但它并没有解决 JavaScript 应用程序的其他常见问题,比如那些被包含但从未使用的代码。为了解决这个问题,我们需要使用摇树(tree shaking)优化技术。

什么是摇树?

摇树是一种消除死代码的方法。这个词最初是由 Rollup 发起的,并逐渐流行开来,但消除死代码的概念却早已存在。webpack 中也涉及了这个概念,本文将通过示例进行演示。

这项技术之所以被称为“摇树”,主要是因为应用程序的依赖项是树状结构。树中的每个节点都代表了一个依赖项,这些依赖项为应用程序提供了不同的功能。在现代应用程序中,这些依赖项通过静态导入语句进行引入,如下所示:

// Import all the array utilities!
import arrayUtils from "array-utils";

在你的应用程序还很年轻的时候(如果你愿意,可以把它叫作“树苗”),应用程序的依赖项相对较少,而且你使用了大多数(如果不是全部)添加的依赖项。但是,随着应用程序的老化,更多的依赖项被添加进来,更糟糕的是,较旧的依赖项不再被使用,但可能无法从代码库中删除。最终的结果就是应用程序会传输大量未使用的 JavaScript 到客户端。摇树利用了静态导入语句来导入 ES6 模块的特定部分,从而解决了这个问题:

// Import only some of the utilities!
import { unique, implode, explode } from "array-utils";

这个示例和之前示例之间的区别在于,它没有从“array-utils”模块中导入所有内容,而是导入它的特定部分。在开发阶段的构建中,这样做并不会有真正的效果,因为不管怎样它都会导入整个模块。但是,在生产阶段的构建中,我们可以通过配置 webpack 让它“摇掉”未明确指定的 ES6 模块,从而减小最终的构建体积。在本文中,你将学会如何做到这一点!

寻找摇树的机会

为了方便说明,我创建了一个单页应用程序示例(https://github.com/malchata/webpack-tree-shaking-example),这个应用程序使用 webpack 来演示摇树的工作原理。如果你愿意,可以拉取这个示例应用程序。不过我们将在本文中一步步介绍这个方法,所以不一定要拉取代码,除非你喜欢边学边动手。

示例应用程序是一个超级简单的吉他踏板数据库搜索程序,输入关键字,可以搜索到吉他踏板的清单。

应用程序的行为被分为 vendor(即 Preact 和 Emotion)和特定于应用程序的代码包(或者在 webpack 中叫作“chunk”):

上图中显示的 JavaScript 包是生产未压缩版本,也就是说它们通过 uglification(http://lisperator.net/uglifyjs/)进行了优化。特定于应用程序的捆绑包的大小为 21.1 KB 算是不错的了。但请注意,这里并没有经过摇树优化。现在让我们来看看应用程序的代码,看看可以做些什么来解决这个问题。

在任何一个应用程序中,在寻找摇树机会时,都会先查找静态导入语句。在主组件文件的顶部附近,你将看到如下所示的行:

import * as utils from "../../utils/utils";

也许你之前也见过这样的东西。导入 ES6 模块的方式有很多,但你要特别注意这个。这行语句好像在说:“导入 utils 模块的所有内容,并把它们放在名称空间 utils 中”。问题是,“这个模块中究竟有多少东西?”

如果你去看一下 utils 模块的源代码,你会发现它包含的东西非常多,可能有 1,300 行代码。

或许所有这些东西都会被用到?事实是这样的吗?让我们搜索一下主组件文件,看看出现了多少 utils 命名空间里的东西。

我们从 utils 导入了大量的模块,但在主组件文件中只调用了三次。

这样不太好。我们只在应用程序代码的三个地方使用了 utils 命名空间里的东西。那么它们是用来实现什么功能的呢?如果再看一下主组件文件,我们会发现,似乎只调用了一个函数,即 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);
}

我们导入了 1,300 行的文件,却只使用了其中一个函数。

需要注意的是,这个示例特意做得这么简单,所以可以很容易地找出膨胀代码的来源。但在包含大量模块的大型项目中,很难找出哪些导入造成了捆绑包的数量激增,不过我们可以借助 Webpack Bundle Analyzer 和 source-map-explorer 这些工具。

这个示例是专门为这篇文章定制的,似乎有点牵强,但在实际的项目当中,遇到这样的情况是不可避免的。也就是说,你已经发现了可以进行摇树的机会了,那么接下来应该怎么做呢?

 不要让 Babel 将 ES6 模块转换为 CommonJS 模块

Babel 是大多数应用程序不可或缺的工具。可惜的是,它会给摇树优化带来一些麻烦。如果使用了 babel-preset-env,它会自动将 ES6 模块转换为 CommonJS 模块(即你 require 的模块,而不是 import 的模块)。这本来是件好事,但在进行摇树优化时问题就来了。

针对 CommonJS 模块进行摇树优化会比较困难,而且 webpack 不知道需要从捆绑中去掉哪些东西。解决方案很简单:在配置 babel-preset-env 时,让它不要处理 ES6 模块。无论你在哪里配置 Babel(无论是.babelrc 还是 package.json),只要增加一些额外的东西:

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ]
}

在 babel-preset-env 配置中指定“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 在修改 fruits 数组时会产生副作用,因为它超出了 addFruit 函数的作用域。

副作用也适用于 ES6 模块,所以会影响到摇树优化。有些模块接收可预测的输入,返回可预测的结果,并且不会修改自身作用域之外的任何东西,如果我们没有使用到这些模块,那么就可以安全地将它们“摇”掉。它们是模块化的独立代码片段。

我们可以在 package.json 文件中指定“sideEffects”: false,告诉 webpack 哪个模块及其依赖项是无副作用的:

{
  "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"
  ]
}

在后一个示例中,未指定的文件将被视为无副作用的。如果你不想在 package.json 文件中添加这些内容,可以在 webpack 配置的 module.rules 中指定(https://github.com/webpack/webpack/issues/6065#issuecomment-351060570)。

只导入需要的东西

之前我们让 Babel 不要处理 ES6 模块,现在需要对导入语法稍作调整,只从 utils 模块中导入我们需要的函数。在本示例中,我们只需要 simpleSort:

import { simpleSort } from "../../utils/utils";

我们像是在说:“只要把 utils 模块中的 simpleSort 给我就行了”。因为我们只将 simpleSort 而不是整个 utils 模块导入到全局作用域,所以需要将 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);
}

现在,我们已经完成了摇树所需的工作。以下是进行摇树优化之前的 webpack 输出:

下面是进行摇树优化后的输出:

两个捆绑包都缩小了,不过 main 捆绑包的缩小幅度更大。通过去掉 utils 模块的未使用部分,我们已经设法从这个捆绑中砍掉了大约 60%的代码。这不仅可以缩短脚本下载所需的时间,还可以缩短处理脚本的时间。

更复杂的场景

在大多数情况下,只要在近期版本的 webpack 中稍作调整就可以进行摇树优化,但总有一些例外情况会让你感到头疼。例如,本文所描述的方法对 lodash 就不起作用。由于 lodash 自身的架构问题,你需要安装 lodash-es(https://www.npmjs.com/package/lodash-es)来代替常规的 lodash,并且使用不同的语法(被叫作“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";

如果你希望保持一致的导入语法,那么可以在使用标准 lodash 包的同时安装 babel-plugin-lodash 插件(http://babel-plugin-lodash/)。在将这个插件添加到 Babel 中后,你可以使用典型的导入语法去掉未使用的依赖项。

如果遇到一个很顽固的库,先看看它是否使用 ES6 语法进行导出。如果它用 CommonJS 格式进行导出(例如 module.exports),那么这些代码将不能通过 webpack 进行摇树优化。有一些插件为 CommonJS 模块提供了摇树功能,例如 webpack-common-shake,但仍然有一些模式的 CommonJS 是无法进行摇树优化的。如果你想要进行可靠的依赖项消除,最好只针对 ES6 模块。

英文原文:

https://developers.google.com/web/fundamentals/performance/optimizing-javascript/tree-shaking/

原文地址:https://www.cnblogs.com/Yanss/p/10231089.html

时间: 2024-08-09 07:14:33

JavaScript性能优化之摇树的相关文章

JavaScript性能优化

1.使用局部变量 在函数中,总是使用var来定义变量.无论何时使用var都会在当前的范围类创建一个局部变量.如果不使用var来定义变量,那么变量会被创建在window范围内,那么每次使用这个变量的时候,解释程序都会搜索整个范围树.同时全局变量要在页面从浏览器中卸载后才销毁,而局部变量在函数执行完毕即可销毁,过多的全局变量增加了不必要的内存消耗.只要有可能就应该用局部变量或者数字索引的数组来替代命名特性.如果命名特性要多次使用,就先将它的值存储在局部变量中,以避免多次使用线性算法请求命名特性的值.

JavaScript性能优化小知识总结(转)

JavaScript的性能问题不容小觑,这就需要我们开发人员在编写JavaScript程序时多注意一些细节,本文非常详细的介绍了一下JavaScript性能优化方面的知识点,绝对是干货. 前言 一直在学习javascript,也有看过<犀利开发Jquery内核详解与实践>,对这本书的评价只有两个字犀利,可能是对javascript理解的还不够透彻异或是自己太笨,更多的是自己不擅于思考懒得思考以至于里面说的一些精髓都没有太深入的理解. 鉴于想让自己有一个提升,进不了一个更加广阔的天地,总得找一个

javascript性能优化-repaint和reflow

repaint(重绘) ,repaint发生更改时,元素的外观被改变,且在没有改变布局的情况下发生,如改变outline,visibility,background color,不会影响到dom结构渲染. reflow(渲染),与repaint区别就是他会影响到dom的结构渲染,同时他会触发repaint,他会改变他本身与所有父辈元素(祖先),这种开销是非常昂贵的,导致性能下降是必然的,页面元素越多效果越明显. 何时发生: 1. DOM元素的添加.修改(内容).删除( Reflow + Repa

[转]JavaScript性能优化

如今主流浏览器都在比拼JavaScript引擎的执行速度,但最终都会达到一个理论极限,即无限接近编译后程序执行速度. 这种情况下决定程序速度的另一个重要因素就是代码本身. 在这里我们会分门别类的介绍JavaScript性能优化的技巧,并提供相应的测试用例,供大家在自己使用的浏览器上验证, 同时会对特定的JavaScript背景知识做一定的介绍. 变量声明带上var 如果声明变量忘记了var,那么js引擎将会遍历整个作用域查找这个变量,结果不管找到与否,都是悲剧. 如果在上级作用域找到了这个变量,

摘:JavaScript性能优化小知识总结

原文地址:http://www.codeceo.com/article/javascript-performance-tips.html JavaScript的性能问题不容小觑,这就需要我们开发人员在编写JavaScript程序时多注意一些细节,本文非常详细的介绍了一下JavaScript性能优化方面的知识点,绝对是干货. 前言 一直在学习javascript,也有看过<犀利开发Jquery内核详解与实践>,对这本书的评价只有两个字犀利,可能是对javascript理解的还不够透彻异或是自己太

JavaScript性能优化小窍门实例汇总

JavaScript性能优化小窍门实例汇总在众多语言中,JavaScript已经占有重要的一席之地,利用JavaScript我们可以做很多事情 , 应用广泛.在web应用项目中,需要大量JavaScript的代码,将来也会越来越多. 但是由于JavaScript是一个作为解释执行的语言,而且它的单线程机制,决定了性能问题是JavaScript的弱点,也是开发者在写JavaScript的时候需注意的一个问题. 因为经常会遇到Web 2.0应用性能欠佳的问题,主因就是JavaScript性能不足,导

JavaScript 性能优化技巧分享

JavaScript 作为当前最为常见的直译式脚本语言,已经广泛应用于 Web 应用开发中.为了提高Web应用的性能,从 JavaScript 的性能优化方向入手,会是一个很好的选择. 本文从加载.上下文.解析.编译.执行和捆绑等多个方面来讲解 JavaScript 的性能优化技巧,以便让更多的前端开发人员掌握这方面知识. 什么是高性能的 JavaScript 代码? 尽管目前没有高性能代码的绝对定义,但却存在一个以用户为中心的性能模型,可以用作参考:RAIL模型. 响应 如果你的应用程序能在1

JavaScript强化教程——javascript性能优化

本文为 H5EDU 机构官方 HTML5培训 教程,主要介绍:JavaScript强化教程 ——数据存取 数据的存取位置也影响着JavaScript的运行速度,我们来总结一下如能分配存取位置最合理,能够高效的运行JavaScript数据存取位置 分为 1.字面量(如true.false.null等) 2.本地变量(var a=5;) 3.数组元素(var arr=[]; arr[0]=5;) 4.对象成员 以上这些都可以存储数据根据不同浏览器进行的200 000次读取数据所需时间得出,由快到慢为

Javascript性能优化(一)

一.加载 1. 依据HTML 4规范,script标签可以放置在head和body标签中的任意位置 2. 下载js脚本会阻塞其他页面文件下载,所以应尽可能将script标签放置在body底部 3. HTML 4为script标签增加了一个defer属性,表明延迟执行,但这并不是标准做法 4. 将多个script合并后压缩,放置在body标签底部,是引入多个外链javascript文件的最佳实践 5. 通过动态创建标签,可以异步引入js文件,代码如下: 1 function loadScript(