JavaScript性能超出捆绑包大小

2021-03-01 02:11:48

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

我认为我们在网络性能社区中存在同样的问题。最近,人们非常关注JavaScript包的大小:您的依赖项有多大?你能用一个较小的吗?您可以延迟加载吗?但是我相信我们首先要重点关注捆绑商品的尺寸,因为它很容易衡量。

并不是说捆绑包的大小并不重要!就像您可能将钥匙遗留在路灯中一样。而且,您最好先检查一下那里,因为它是最快的查找地点。但是,还有其他一些事情很难衡量,但可能同样重要:

JavaScript依赖关系可能会影响所有这些指标。但是,与讨论包大小相比,对它们的讨论较少,我怀疑这是因为它们的度量不那么直接。在这篇文章中,我想谈谈我如何处理捆绑商品规模,以及我如何也应对其他指标。

在谈论JavaScript代码的大小时,您必须精确。有些人会说“我的图书馆有10 KB。”缩小了吗?压缩吗?摇树?您是否使用了最高的Gzip设置(9)? Brotli压缩又如何呢?

这听起来像头发劈开,但是区别实际上很重要,尤其是在压缩尺寸和未压缩尺寸之间。压缩后的大小会影响通过网络发送字节的速度,而未压缩后的大小会影响浏览器解析,编译和执行JavaScript所花费的时间。 (尽管这并不是一个完美的预测指标,但它们往往与代码大小相关。)

不过,最重要的是保持一致。您不想使用最小化和未压缩的大小来衡量库A,而不想使用最小化和压缩的大小来衡量库B(除非在服务方式上存在真正的差异)。

对我来说,Bundlephobia是瑞士军团尺寸分析的利器。您可以从npm查找任何依赖项,它将告诉您最小大小(浏览器解析和执行的内容)以及最小和压缩大小(浏览器下载的内容)。

例如,我们可以使用此工具来查看最小反应作用权重为121.1kB,而预先执行权重为10.2kB。因此,我们可以确认Preact确实是诚实的货–一个与React兼容的框架,尺寸很小!

在这种情况下,我不会迷上确切的Minifier或Bundlephobia所使用的Gzip压缩级别,因为至少它在所有地方都使用相同的系统。所以我知道我在比较苹果与苹果。

它并没有告诉您巨额的费用。如果您仅导入模块的一部分,则其他部分可能会被树遮盖掉。

它不会告诉您有关子目录的依赖性。因此,举例来说,我知道导入preact / compat多么昂贵,但导入preact / compat&compat'几乎可以是任何东西– compat.js可能是一个巨大的文件,我无法知道。

如果涉及到polyfill(例如,捆绑程序为Node的Buffer API或JavaScript Object.assign()API注入了polyfill),则您不一定会在这里看到它。

在上述所有情况下,您实际上只需要运行捆绑程序并检查输出即可。每个捆绑器都不同,根据配置或其他因素,您可能最终会得到一个巨大的捆绑包或一个很小的捆绑包。接下来,让我们继续使用捆绑程序专用的工具。

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

就显示的大小而言,两个最有用的大小是“解析”(默认)和“压缩”。 “已解析”本质上是指“最小化”,因此,这两个度量与Bundlephobia告诉我们的大致可比。但是这里的区别在于我们实际上是在运行捆绑程序,因此我们知道大小对于我们的特定应用程序是准确的。

对于汇总,我真的很想拥有一个图形界面,例如Webpack Bundle Analyzer。但是,我发现的下一个最好的东西是Rollup Plugin Analyer,它将在构建时将模块大小输出到控制台。

不幸的是,此工具并没有为我们提供缩小的尺寸或压缩的尺寸-只是在进行此类优化之前Rollup看到的尺寸。它并不完美,但紧要关头很棒。

但是,正如我所提到的,我认为JavaScript包的大小并不决定一切。第一次比较好,因为它比较容易衡量,但是还有很多其他指标会影响网页性能。

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

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

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

此代码片段在Bundlephobia上得分很高,但不幸的是它将阻塞主线程5秒钟。这是一个有点荒谬的示例,但是在现实世界中,您会找到小型库,但是它们仍然会破坏主线程。遍历DOM中的所有元素,遍历LocalStorage中的一个大型数组,计算pi的位数……除非您亲自检查了所有依赖项,否则很难知道它们在其中所做的工作。

解析和编译都非常难以衡量。愚弄自己很容易,因为浏览器在字节码缓存方面做了很多优化。例如,浏览器可能不会在第二页加载或第三页加载(!)时,或者在Service Worker中缓存JavaScript时不运行解析/编译步骤。因此,当浏览器确实预先缓存了模块时,您可能会认为模块的解析/编译便宜。

保证100%安全的唯一方法是完全清除浏览器缓存并衡量首页加载。我不想四处乱逛,因此通常我会在私人/访客浏览窗口或完全独立的浏览器中执行此操作。您还需要确保禁用所有浏览器扩展程序(通常使用私有模式),因为这些扩展程序可能会影响页面加载时间。您不想半途而废地分析Chrome跟踪并意识到自己正在测量密码管理器!

我通常要做的另一件事是将Chrome的CPU限制设置为4倍或6倍。我将4x视为“与移动设备足够相似”,将6x视为“一种超高速减慢的机器,它使迹线更容易阅读,因为所有内容都更大。”使用您想要的任何一种;与您的(可能是)高端开发人员计算机相比,这两种方法都更能代表实际用户。

如果我担心网络速度,那么在这一点上我也将打开网络限制。 “快速3G”通常是一个不错的选择,它会达到“更像现实世界”和“不慢到我开始对计算机大吼大叫”之间的甜蜜点。

如有必要,请导航至about:blank(您不想衡量浏览器主页的卸载事件)。

现在,您有了性能跟踪(也称为“时间轴”或“配置文件”),它将向您显示初始页面加载中JavaScript代码的解析/编译/执行时间。不幸的是,这部分最终可能是相当手工的,但是有一些技巧可以使它变得更容易。

最重要的是,使用User Timing API(也称为性能标记和度量)使用对您有意义的名称来标记Web应用程序的各个部分。专注于您担心会很昂贵的部分,例如根应用程序的初始渲染,阻塞的XHR调用或引导状态对象。

如果您担心这些API的(少量)开销,可以在生产中删除Performance.mark / Performance.measure调用。我喜欢根据查询字符串参数打开或关闭它,这样,如果我想分析生产版本,就可以轻松地打开生产中的用户计时。缩小时,Terser的pure_funcs选项还可用于删除performance.mark和performance.measure调用。 (哎呀,您也可以在此处删除console.logs。非常方便。)

另一个有用的工具是mark-loader,它是一个Webpack插件,可以自动将模块包装在mark / measure调用中,以便您可以查看每个依赖项的运行时成本。当工具可以确切告诉您哪些依赖项正在消耗多少时间时,为什么还要对JavaScript调用栈感到困惑呢?

衡量运行时性能时要注意的一件事是,成本在最小化代码和未最小化代码之间可能会有所不同。未使用的功能可能会被删除,代码会更小,更优化,并且库可能会定义process.env.NODE_ENV ===' development'不在生产模式下运行的块

我处理这种情况的一般策略是将缩小的生产结构视为真理的来源,并使用标记和措施使之易于理解。但是,如上所述,performance.mark和performance.measure都有自己的小开销,因此您可能需要使用查询字符串参数进行切换。

您不必是环保主义者,就认为最大限度地减少用电很重要。我们生活在一个世界上,人们越来越多地在未插入电源插座的设备上浏览网络,而他们想要做的最后一件事就是因为网站行为不当而用光了果汁。

我倾向于将功耗作为CPU使用率的一个子集。这有一些例外,例如唤醒无线电以建立网络连接,但是大多数情况下,如果网站消耗过多的电量,那是因为它在主线程上消耗了过多的CPU。

因此,我上面提到的有关改善JavaScript解析/编译/执行时间的所有内容,也将减少功耗。但是,特别是对于寿命长的Web应用程序,最隐蔽的功耗形式是在第一页加载之后出现的。这可能表现为用户突然注意到他们的笔记本电脑风扇在呼or或手机变热,即使他们只是看着一个(显然)空闲的网页。

在这种情况下,再次选择的工具是“ Chrome DevTools性能”选项卡,使用与上述基本相同的步骤。不过,您要查找的是重复使用CPU,通常是由于计时器或动画引起的。例如,编码不良的自定义滚动条,IntersectionObserver polyfill或动画加载微调器可能会决定他们需要在每个requestAnimationFrame或setInterval循环中运行代码。

请注意,由于未优化的CSS动画,也可能发生这种耗电–不需要JavaScript! (在那种情况下,在Chrome UI中将是紫色峰而不是黄色峰。)对于长时间运行的CSS动画,请确保始终喜欢GPU加速的CSS属性。

您可以使用的另一个工具是Chrome的“效果监视器”标签,该标签实际上与“性能”标签不同。我认为这是一种对您的网站性能进行监控的心跳监视器,而无需手动启动和停止跟踪。如果您在其他网页上看到的CPU使用率稳定,则可能是电源使用问题。

另外:向WebKit专家介绍技巧,后者向Safari Web Inspector添加了显式的Energy Impact面板。签出的另一个好工具!

内存使用情况以前很难分析,但是最近工具有了很大的改进。

去年,我已经写了一篇有关内存泄漏的文章,但是请记住,内存使用和内存泄漏是两个独立的问题,这一点很重要。网站可以具有较高的内存使用率,而不会显式泄漏内存。另一个网站可能从很小的地方开始,但由于失控的泄漏,最终使网站膨胀到了巨大的规模。

您可以阅读上面的博客文章,了解如何分析内存泄漏。但是就内存使用而言,我们有一个新的浏览器API,它在测量方面有很大帮助:performance.measureUserAgentSpecificMemory(以前称为performance.measureMemory,可悲的是,它要少很多)。此API有几个优点:

它返回一个在垃圾回收后自动解决的承诺。 (不再需要使用奇怪的技巧来强制执行GC!)

它不仅测量JavaScript VM的大小,还包括DOM内存以及Web Worker和iframe中的内存。

如果是跨网站的iframe(由于网站隔离而被流程隔离),则会破坏归因。因此,您可以确切地知道广告和嵌入的存储空间多么庞大!

在这种情况下,字节是您要用于“我正在使用多少内存?”的标语指标。细分是可选的,规范明确指出浏览器可以决定不包括它。

也就是说,使用此API可能仍然很挑剔。首先,它仅在Chrome 89+中可用。 (在稍早的发行版中,您可以设置“启用实验性Web平台功能”标志并使用旧的performance.measureMemory API。)但更多的问题是,由于存在滥用的可能性,该API仅限跨平台使用。起源隔离的上下文。这实际上意味着您必须设置一些特殊的标头,并且如果您依赖任何跨域资源(外部CSS,JavaScript,图像等),它们也需要设置一些特殊的标头。

但是,如果这听起来太麻烦了,并且仅打算使用此API进行自动化测试,则可以使用--disable-web-security标志运行Chrome。 (当然,后果自负!)但是请注意,目前在无头模式下无法测量内存。

当然,此API也无法为您提供更高的粒度。例如,您将无法确定React占用了X个字节,Lodash占用了Y个字节,依此类推。A / B测试可能是找出此类问题的唯一有效方法。但这仍然比我们用于测量内存的较旧工具要好得多(该工具存在很大缺陷,甚至根本不值得描述)。

在网络应用程序场景中,限制磁盘使用量是最重要的,在这种情况下,可能会达到浏览器配额限制,具体取决于设备上的可用存储量。过多的存储使用可能有多种形式,例如将太多大图像填充到ServiceWorker缓存中,但是JavaScript也可以累加。

您可能会认为JavaScript模块的磁盘使用量与其包大小(即缓存它的成本)有直接关系,但是在某些情况下,这是不正确的。例如,通过我自己的emoji-picker-element,我大量使用IndexedDB来存储emoji数据。这意味着我必须意识到与数据库相关的磁盘使用情况,例如存储不必要的数据或创建过多的索引。

Chrome DevTools具有“应用程序”标签,该标签显示了网站的总存储空间使用情况。作为一个大概的近似值,这很好,但是我发现此屏幕可能有点不一致,并且必须手动收集数据。另外,我不仅对Chrome感兴趣,还因为IndexedDB在各种浏览器中的实现方式大不相同,因此存储大小可能会千差万别。

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

举个例子,这是我对当前版本的emoji-picker-element的理解:

当然,如果要测量ServiceWorker缓存,LocalStorage等的存储大小,则必须调整此脚本。

可能在生产环境中更好工作的另一个选项是StorageManager.estimate()API。但是,它的目的更多是为了弄清您是否正在接近配额限制而不是性能分析,因此我不确定作为磁盘使用率指标的准确性如何。正如MDN所说:“返回的值不正确;出于安全原因,压缩,重复数据删除和混淆之间的关系将是不精确的。”

性能是多方面的。如果我们可以将其缩减为单个度量标准(例如捆绑包大小),那将是很好的选择,但是如果您真的想涵盖所有基础,则需要考虑很多不同的角度。

有时候,这可能让人感到不知所措,这就是为什么我认为诸如Core Web Vitals之类的计划或通常专注于捆绑销售规模的计划并不是一件坏事。如果您告诉人们他们需要优化许多不同的指标,那么他们可能会决定不对其进行优化。

就是说,特别是对于JavaScript依赖关系,如果一眼就能看到所有这些指标,我将很乐意。想象一下,如果Bundlephobia具有“营养事实”类型的视图,并且将bundle size作为标题度量标准(有点像卡路里!),并且下面列出了所有其他度量标准。不必很精确:数字可能取决于浏览器,DOM的大小,API的使用方式等。但是您可以想象一些有关初始CPU执行时间,内存使用率和磁盘使用率的基本统计信息以自动化的方式衡量并非不可能。

如果存在这种情况,那么就可以明智地决定要使用哪些JavaScript依赖项,是否延迟加载它们,等等。但是与此同时,有很多不同的方式来收集这些数据,希望这篇博客文章至少鼓励您将目光移到路灯之外。

感谢Thomas Steiner和Jake Archibald对本文的草稿提供了反馈。