[译]JavaScript 调优:如何处理 bundle 大小 | 技术点评

1,529 阅读17分钟

本文翻译自:nolanlawson.com/2021/02/23/…

一个古老的故事,关于一个醉汉试图在路灯下找到他的钥匙。为什么?因为那是最明亮的地方。这是一个有趣的故事,但也是有关联的,因为作为人类,我们都倾向于走阻力最小的道路。

我认为我们在网络性能社区也有同样的问题。最近人们非常关注 JavaScript bundle 大小:你的依赖有多大?你能使用一个更小的吗?你能懒惰加载吗?但是我相信我们首先关注 bundle 大小,因为它很容易测量。

这并不是说包的大小不重要!就像你可能把钥匙留在路灯上一样。见鬼,你不妨先检查一下那里,因为那里是最快的地方。但是这里还有一些其他的东西很难测量,但也同样重要:

  • Parse/compile时间
  • 执行时间
  • 电力使用
  • 内存使用
  • 磁盘使用

JavaScript依赖性会影响所有这些指标。但是它们比 bundle 大小讨论得少,我怀疑这是因为它们不太容易测量。在这篇文章中,我想谈谈我如何处理bundle大小,以及我如何处理其他指标。

束大小

当谈到JavaScript代码的大小时,你必须精确。有些人会说“我的库是10千字节。”那是缩小的吗? Tree-shaken?你使用了最高的 Gzip 设置(9)吗? Brotli压缩呢?

这听起来很头疼,但区别实际上很重要,尤其是压缩和未压缩大小之间的区别。压缩大小影响通过电线发送字节的速度,而未压缩大小影响浏览器解析、编译和执行JavaScript所需的时间。(这些往往与代码大小相关,尽管它不是一个完美的预测器。)

然而,最重要的是保持一致。你不想用未缩小、未压缩的大小来衡量库A,而用缩小和压缩的大小来衡量库B(除非你为它们提供的服务有真正的不同)。

捆绑恐惧症

对我来说,捆绑恐惧症是捆绑大小分析的瑞士军刀。你可以从npm中查找任何依赖项,它会告诉你缩小的大小(浏览器解析和执行的内容)以及缩小和压缩的大小(浏览器下载的内容)。

例如,我们可以使用这个工具看到[react-dom](https://bundlephobia.com/result? p=react-dom@17.0.1)的重量缩小了121.1kB,但是[preact](https://bundlephobia.com/result? p=preact@10.5.12)重10.2kB。所以我们可以确认Preact真的是诚实的商品——一个很小的反应兼容框架!

在这种情况下,我不会纠结于到底是哪个迷你程序或者到底是什么Gzip压缩级别的捆绑恐惧症,因为至少它在任何地方都使用相同的系统。所以我知道我在比较苹果和苹果。

话虽如此,邦德恐惧症有一些警告:

  1. 它没有告诉你树形调整的成本。如果你只导入一个模块的一部分,其他部分可能会被树形调整掉。
  2. 它不会告诉你子目录依赖关系。例如,我知道import 'preact'有多贵,但是import 'preact/compat'可以是任何东西——compat.js可能是一个巨大的文件,我无法知道。
  3. 如果涉及到多填充(例如,您的bundler为Node的BufferAPI或JavaScriptObject.assign()API注入了多填充),您不一定会在这里看到它。

在上述所有情况下,您只需要运行bundler并检查输出。每个bundler都是不同的,根据配置或其他因素,您可能最终会得到一个大的bundler或一个小的bundler。所以接下来,让我们继续讨论特定于bundler的工具。

WebpackBundle分析仪

我喜欢WebpackBundle Analyzer。它提供了Webpack输出中每个块的良好可视化,以及这些块中的哪些模块。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-20-09-45-39.png? w=570&h=227

它显示的大小而言,最有用的两个是“解析”(默认值)和“Gzip." "解析”本质上意味着“缩小”,所以这两个测量值与捆绑恐惧症告诉我们的大致相当。但这里的区别是,我们实际上是在运行捆绑程序,所以我们知道这些大小对于我们特定的应用程序是准确的。

Rollup插件分析仪

对于Rollup,我真的很想有一个像WebpackBundle Analyzer这样的图形界面。但是我发现的下一个最好的东西是Rollup插件分析器,它会在构建时将您的模块大小输出到控制台。

不幸的是,这个工具没有给我们缩小或压缩的大小——只是在这种优化发生之前由Rollup看到的大小。它不完美,但在紧要关头它很棒。

其他捆绑大小的工具

我曾涉足并发现有用的其他工具:

我相信你可以找到其他工具来添加到这个列表中!

超越捆绑

正如我提到的,我不认为JavaScript bundle大小就是一切。作为第一近似值,它很棒,因为它(相对)容易测量,但是有很多其他指标可以影响页面性能。

运行时CPU成本

第一个也是最重要的一个是运行时成本。这可以分成几个桶:

  • 解析
  • 汇编,汇编
  • 行刑

这三个阶段基本上是调用require("some-dependency")import "some-dependency"的端到端成本。它们可能与捆绑大小相关,但不是一对一的映射。

举个简单的例子,这里有一个(微小的!)消耗大量CPU的JavaScript代码段:

const start=Date. now ()

while (Date.now() - start < 5000) {}

这个代码段在Bundlephessa上会获得很高的分数,但不幸的是,它会阻塞主线程5秒钟。这是一个有点荒谬的例子,但在现实世界中,您可以找到尽管如此锤击主线程的小库。遍历DOM中的所有元素,在LocalStore中迭代一个大数组,计算pi的数字... 除非你亲自检查了你所有的依赖,否则很难知道他们在里面做什么。

解析和编译都很难衡量。很容易欺骗自己,因为浏览器对字节码缓存有很多优化。例如,浏览器可能不会在第二页加载或第三页加载时运行parse/compile第三页加载时运行步骤 (!), 或者JavaScript缓存在Service Worker中。所以你可能会认为一个模块parse/compile便宜,而浏览器只是提前缓存了它。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-22-21-02-33.png? w=570&h=240

ChromeDevTools中的编译和执行。请注意,Chrome在主线程之外进行一些解析和编译。

100%安全的唯一方法是完全清除浏览器缓存并测量第一页加载。我不喜欢胡闹,所以通常我会在private/guest浏览窗口或完全独立的浏览器中这样做。您还需要确保禁用任何浏览器扩展(私有模式通常会这样做),因为这些扩展会影响页面加载时间。您不想在分析Chrome跟踪的中途意识到您正在测量您的密码管理器!

我通常做的另一件事是将Chrome的中央处理器节流设置为4倍或6倍。我认为4x“足够类似于移动设备”,6x是“一台超级骗子减慢速度的机器,它使痕迹更容易阅读,因为一切都更大。”使用你想要的任何一个;两者都比你(可能)的高端开发者机器更能代表真实用户。

如果我担心网络速度,这也是我打开网络节流的地方。“快速3G”通常是一个很好的选择,它在“更像现实世界”和“不慢到我开始对电脑大喊大叫”之间找到了最佳位置

所以综合起来,我获得准确跟踪的步骤通常是:

  1. 打开private/guest浏览窗口。
  2. 如有必要,导航到about:blank(您不想测量浏览器主页的unload事件)。
  3. 在Chrome中打开DevTools。
  4. 转到性能选项卡。
  5. 在设置中,打开CPU节流and/or网络节流。
  6. 单击记录按钮。
  7. 键入URL并按回车键。
  8. 加载页面后停止记录。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-20-14-58-18.png? w=570&h=324

现在您有了一个性能跟踪(也称为“时间线”或“配置文件"), ,它将向您显示JavaScript代码在初始页面加载中的parse/compile/execution时间。不幸的是,这部分最终可能会非常手动,但有一些技巧可以让它变得更容易。

最重要的是,使用用户计时应用编程接口(又称性能标记和度量)将网络应用程序的部分标记为对您有意义的名称。专注于您担心会昂贵的部分,例如根应用程序的初始渲染、阻塞XHR调用或引导您的状态对象。

如果您担心这些API的(小)开销,您可以在生产中剔除性能performance.mark/performance.measure测量调用。我喜欢根据查询字符串参数打开或关闭它,这样如果我想分析生产构建,我可以很容易地打开生产中的用户计时。Terser的[pure_funcs选项](terser.org/docs/api-re…用来在缩小时删除performance.markperformance.measure调用。(见鬼,你也可以删除console.log。非常方便。)

另一个有用的工具是[mark-loader](https://github.com/statianzo/mark-loader),它是一个Webpack插件,可以自动将您的模块包装在mark/measure调用中,这样您就可以看到每个依赖项的运行时成本。为什么要在JavaScript调用堆栈上费解,因为该工具可以准确地告诉您哪些依赖项消耗了多少时间?

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-22-22-02-23.png? w=570&h=279

在生产模式下加载三个. js、时刻和反应。如果没有用户计时,你能找出时间花在哪里吗?

在测量运行时性能时需要注意的一件事是,成本在精简和未精简的代码之间可能会有所不同。未使用的函数可能会被剥离,代码会更小、更优化,库可能会定义不在生产模式下运行的process.env. NODE_ENV === 'development'块。

我处理这种情况的一般策略是将缩小的生产构建视为真理的来源,并使用标记和度量来使其易于理解。但是,如前所述,performance.markperformance.measure有自己的小开销,因此您可能希望使用查询字符串参数来切换它们。

电力使用

你不必是一个环保主义者,就能认为最大限度地减少电力使用是重要的。我们生活在一个越来越多的人用不插电源插座的设备浏览网络的世界里,他们最不想看到的就是因为一个行为不端的网站而耗尽电力。

我倾向于认为电源使用是CPU使用的一个子集。这有一些例外,比如唤醒收音机来进行网络连接,但大多数时候,如果一个网站消耗过多的电源,那是因为它在主线程上消耗了过多的CPU。

因此,我上面所说的关于改进JavaScriptparse/compile/execute时间的一切也将降低功耗。但是对于长寿命的网络应用程序来说,最阴险的耗电形式是在第一页加载之后。这可能表现为用户突然注意到他们的笔记本电脑风扇在嗡嗡作响,或者他们的手机越来越热,即使他们只是在看一个(显然)空闲的网页。

同样,在这种情况下,首选的工具是Chrome开发工具性能选项卡,使用的基本上与上述步骤相同。然而,您需要寻找的是重复的CPU使用,通常是由于计时器或动画。例如,编码不佳的自定义滚动条、Intersection观测器多填充或动画加载旋转器可能会决定它们需要在每个requestAnimationFramesetInterval循环中运行代码。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-20-15-19-13.png? w=570&h=224

一个表现不佳的JavaScript小部件。注意JavaScript使用的小高峰,即使页面空闲,它也显示出持续的CPU使用。

请注意,由于未优化的CSS动画,这种功耗也可能发生——不需要JavaScript!(在这种情况下,Chrome用户界面中的峰值将是紫色的,而不是黄色的。)对于长期运行的CSS动画,请确保始终首选GPU加速的CSS属性。

您可以使用的另一个工具是Chrome的性能监视器选项卡,它实际上不同于性能选项卡。我认为这是一种心跳监视器,可以显示您的网站在性能方面的表现,而无需手动启动和停止跟踪。如果您在一个惰性的网页上看到持续的CPU使用,那么您可能会遇到电源使用问题。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-20-15-25-10.png? w=570&h=317

性能监视器中同样表现不佳的JavaScript小部件。注意CPU使用的持续低嗡嗡声,以及内存使用中的锯齿模式,表明内存不断被分配和取消分配。

另外:向WebKit的人致敬,他们为Safari Web检查员添加了一个明确的能源影响面板。另一个值得一看的好工具!

内存使用

内存使用曾经是一个很难分析的东西,但是工具最近有了很大的改进。

去年我已经写了一篇关于内存泄漏的文章,但是重要的是要记住内存使用和内存泄漏是两个独立的问题。一个网站可以在没有明确泄露内存的情况下拥有高内存使用率。而另一个网站可能从小处开始,但最终会因为失控的泄漏而膨胀到巨大的规模。

您可以阅读上面的博客文章,了解如何分析内存泄漏。但是就内存使用而言,我们有了一个新的浏览器应用编程接口,它在测量内存方面有很大帮助:[performance.measureUserAgentSpecificMemory](https://www.chromestatus.com/feature/5685965186138112)(以前的performance.measureMemory,遗憾的是,它少得多)。这个应用编程接口有几个优点:

  1. 它返回一个承诺,在垃圾收集后自动解析。(不再需要奇怪的黑客来强制GC!)
  2. 它测量的不仅仅是JavaScript VM大小,它还包括DOM内存以及网络工作人员和iframe中的内存。
  3. 在跨源框架的情况下,由于站点隔离而被过程隔离,它将分解属性。所以你可以确切地知道你的广告和嵌入有多需要内存!

以下是API的示例输出:

{
  "breakdown": [
    {
      "attribution": ["https://pinafore.social/"],
      "bytes": 755360,
      "types": ["Window", "JS"]
    },
    {
      "attribution": [],
      "bytes": 804322,
      "types": ["Window", "JS", "Shared"]
    }
  ],
  "bytes": 1559682
}

在这种情况下,bytes是您要用于“我使用了多少内存”的横幅指标breakdown是可选的,规范明确指出浏览器可以决定不包括它

也就是说,使用这个应用编程接口仍然是很挑剔的。首先,它只在Chrome89中可用+. (在稍旧的版本中,您可以设置“启用实验网络平台功能”标志并使用旧的performance.measureMemory。)然而,更成问题的是,由于滥用的可能性,该应用编程接口仅限于跨源隔离的上下文。这实际上意味着您必须设置一些特殊标题,如果您依赖任何跨源资源(外部CSS、JavaScript、图像等.), 他们也需要设置一些特殊标题。

不过,如果这听起来太麻烦了,并且如果您只计划将此应用编程接口用于自动测试,那么您可以使用禁用-网络安全标志](禁用-网络安全标志)运行Chrome[--disable-web-security标志](stackoverflow.com/a/58658101/…运行 . (当然,风险自负!)不过,请注意,测量内存目前并不在无头模式下工作

当然,这个应用编程接口也没有给你很大的粒度。例如,你无法弄清楚反应占用了X个字节,Lodash占用了Y个字节,等等。A/B测试可能是解决这种问题的唯一有效方法。但这仍然比我们用来测量内存的旧工具好得多(它有如此多的缺陷,甚至不值得描述)。

磁盘使用

限制磁盘使用在Web应用程序场景中是最重要的,在该场景中,可以根据设备上的可用存储量达到浏览器配额限制。过度的存储使用可以有多种形式,例如将太多的大型映像填充到ServiceWorker缓存中,但是JavaScript也可以累加。

你可能会认为JavaScript模块的磁盘使用与其bundle大小(即缓存它的成本)直接相关,但也有一些情况是不正确的。例如,对于我自己的[emoji-picker-element](https://github.com/nolanlawson/emoji-picker-element),我大量使用索引数据库来存储表情数据。这意味着我必须了解与数据库相关的磁盘使用情况,例如存储不必要的数据或创建过多的索引。

https://nolanwlawson.files.wordpress.com/2021/02/screenshot-from-2021-02-20-16-07-19.png? w=570

Chrome开发工具有一个“应用程序”选项卡,显示网站的总存储使用量。作为第一个近似值,这很好,但是我发现这个屏幕可能有点不一致,数据也必须手动收集。此外,我感兴趣的不仅仅是Chrome,因为索引数据库在浏览器之间的实现有很大的不同,所以存储大小可能会有很大的不同。

我找到的解决方案是一个启动Playwright的小脚本Playwright的小脚本,这是一个类似Puppeteer的工具,它的优点是能够启动更多的浏览器,而不仅仅是Chrome。另一个巧妙的功能是它可以启动带有新存储区域的浏览器,所以你可以启动一个浏览器,将存储写入/tmp,然后测量每个浏览器的IndexedDB使用情况。

举个例子,以下是我对当前版本emoji-picker-element看法:

无标题的

当然,如果您想测量ServiceWorker缓存、LocalStore等的存储大小,则必须修改此脚本。

另一个在生产环境中可能更好的选择是[StorageManager.estimate()](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/estimate)应用编程接口。然而,这更多的是为了确定你是否接近配额限制,而不是性能分析,所以我不确定它作为磁盘使用指标的准确性。正如MDN指出的:“返回的值不精确;出于安全原因,在压缩、重复数据删除和模糊处理之间,它们将是不精确的。”

结论

性能是一个多方面的事情。如果我们能把它减少到一个单一的指标,比如bundle大小,那就太好了,但是如果你真的想涵盖所有的基础,有很多不同的角度需要考虑。

有时这会让人感到难以承受,这就是为什么我认为像核心网络生命体征这样的计划,或者对bundle大小的普遍关注,并不是一件坏事。如果你告诉人们他们需要优化十几个不同的指标,他们可能会决定不优化其中的任何一个。

也就是说,特别是对于JavaScript依赖,我希望能更容易地一目了然地看到所有这些指标。想象一下,如果邦德恐惧症有一个“营养事实”类型的视图,以捆绑大小作为标题指标(有点像卡路里!), 和下面列出的所有其他指标。它不一定要精确:数字可能取决于浏览器、DOM的大小、应用编程接口的使用方式等。但是你可以想象一些关于初始CPU执行时间、内存使用和磁盘使用的基本统计数据,这些数据不可能以自动化的方式测量。

如果有这样的东西存在,那么就更容易做出明智的决定,决定使用哪些JavaScript依赖项,是否懒惰加载它们,等等。但与此同时,有许多不同的方法来收集这些数据,我希望这篇博客文章至少鼓励你超越街灯。

感谢托马斯·施泰纳杰克·阿奇博尔德对这篇博客文章草稿的反馈。

本文正在参与「掘金 3 月闯关活动」,点击查看 活动详情