在Superhuman,我们正在构建世界上最快的电子邮件体验。通过收件箱的速度是以前的两倍,并保持收件箱为零!
一般来说:“如果无法衡量,就无法改善”
所以,我们花费大量时间来测量速度。结果证明,性能指标出人意料地具有挑战性。
一方面,很难设计出准确代表用户体验的指标,另一方面,很难制定有用的精确指标。结果,许多团队无法信任他们的性能数据。
即使具有准确和精确的指标,数据也难以使用。我们如何定义“快速”?我们如何平衡速度和一致性?我们如何快速找到回归或查看优化的影响?
在这篇文章中,我们分享了如何为快速的Web应用程序建立性能指标。
1.使用正确的计时器
Javascript有两个计时器:performance.now()和new Date()
有什么不同?对我们来说,有两个:
performance.now()更加精确。new Date()精确度为±1ms,而performance.now()精确度为±100μs。(是的,微秒!)
performance.now()恒定递增。换句话说,它不会受到系统时间的影响。如果您的计算机时间改变了,那么它new Date()也会改变,从而破坏您的数据。但是performance.now()只会以一个恒定的速率慢慢增加。
显然performance.now()是更好的计时器,但它并不完美。当机器进入睡眠状态时,它们同样面临着相同的问题:测量数据包括机器不处工作状态的时间。
2.确保您的应用程序处于前台运行
如果您的用户切换到另一个浏览器选项卡,则指标将中断。为什么?因为器会限制在后台运行的应用程序的CPU。
现在,有两种情况会影响我们的指标,并使结果看上去比实际情况要慢得多:
机器进入休眠状态
该应用程序在后台标签中运行
这两种情况都很可能。幸运的是,我们有两种可能的解决方案。
首先,我们可以通过降低离谱的数字来简单地忽略扭曲的指标
其次,我们可以使用document.hidden和visibilitychange 事件。当用户切换至选项卡或从选项卡切换,最小化或还原浏览器以及计算机从睡眠状态唤醒时,将触发此事件。换句话说,它确实满足了我们的需求。并且选项卡在后台时document.hidden为true。
这是一个简单的实现:
let lastVisibilityChange = 0
window.addEventListener('visibilitychange',()=> {
lastVisibilityChange = performance.now()
})
// 不记录在最近一次可见性更改之前启动的任何
// 如果页面被隐藏,
if( metric.start <lastVisibilityChange || document.hidden)return
我们正在丢弃计算机未全速运行应用程序时的数据。
尽管这是我们不关心的数据,但我们确实关心许多其他交互。让我们看看如何衡量这些。
3.找到最佳活动开始时间
JavaScript中最具争议的功能之一是事件循环是单线程的。一次只能运行一段代码,并且不能中断。
如果用户在代码运行时按了一个键,那么直到代码完成后您才会知道。例如,如果您的应用程序在一个严格的循环中花费了1000毫秒,而您的用户在100毫秒后点击了Escape,你就不会再多注册900毫秒!
这确实会扭曲我们的指标。如果我们想准确地衡量用户的感知,这是一个巨大的问题!
幸运的是,有一个简单的解决方案。如果存在当前事件,则使用performance.now()(window.event.timeStamp系统记录事件的时间)而不是使用(我们看到事件的时间)。
事件时间戳是由浏览器的主进程设置的。由于在阻止事件循环时不会阻止此过程,因此event.timeStamp可以更好地了解事件实际开始的时间。
但是这仍然不是完美的。物理按键与到达Chrome的事件之间仍然存在9–15ms的未知延迟。(帕维尔·法廷(Pavel Fatin)的一篇令人难以置信的文章解释了为什么这样做需要时间。)
但是,即使我们可以测量事件到达Chrome之前的时间,也不应将此时间包含在指标中。为什么?因为我们无法进行代码更改,因此不会显着影响此延迟。这是不可行的。
看来这event.timeStamp是最好的起点。
最好的地方在哪里?
4.在requestAnimationFrame()中完成
JavaScript事件循环还有另一个后果:不相关的代码可以在代码之后但在浏览器显示更新之前运行。
考虑React。代码运行后,React将更新DOM。如果仅在代码中测量时间,那么您将错过React中的时间。
为了测量额外的时间,我们requestAnimationFrame()仅在浏览器准备好刷新框架时才使用计时器:
requestAnimationFrame(()=> {metric.finish(performance.now())})
如图所示,requestAnimationFrame()在CPU工作完成之后以及渲染帧之前运行。如果我们在这里结束计时器,那么我们很有信心在屏幕更新之前一直都将其包括在内。
到目前为止,一切都很好!但是现在情况将变得相当复杂……
5.忽略布局和涂漆时间
框架图说明了另一个问题。在框架结束之前,浏览器必须进行布局和绘制。如果我们不包括这些内容,那么我们测得的时间仍将少于更新显示在屏幕上的时间。
幸运的是,requestAnimationFrame它还袖手旁观。调用它时,会传递一个时间戳,该时间戳是当前帧的开始。时间戳通常在前一帧结束后不久。
因此,我们可以通过测量从event.timeStamp开始到下一帧开始的总时间来弥补这一不足。注意嵌套requestAnimationFrame:
requestAnimationFrame(()=> {
requestAnimationFrame((timestamp)=> {metric.finish(timestamp)})
})
尽管这似乎是一个不错的解决方案,但我们最终没有使用它。因为尽管它提高了精度,却降低了精度。这些时间最多可以与Chrome帧频一样精确。Chrome框架每16ms运行一次,因此最佳精度为±16ms。而且,如果浏览器超载并丢帧,其精度将比这差得难以预测。
如果实施此解决方案,则主要的性能改进(例如将32ms任务减少15ms)不会对指标产生任何影响。
通过跳过“布局”和“绘画”,我们为可以控制的代码获得了更为精确的指标(±100μs)。每一项改进都会显示在数字上。
我们还探讨了一个相关的想法:
requestAnimationFrame(()=> {
setTimeout(()=> {metric.finish(performance.now())}
})
这将包括渲染时间,并且也不会被量化为16ms。但是,我们决定也不使用这种方法:如果输入事件很长,则setTimeouts可能会大大延迟,直到UI更新之后。
6.衡量“未达到目标的事件所占的百分比”
在构建性能时,我们正在尝试优化两件事:
速度。最快的任务应尽可能接近0ms。
一致性。最慢的任务应尽可能接近最快的任务。
由于这两个变量都随时间变化,因此很难可视化和推理。我们可以设计一种可视化方法来鼓励我们优化速度和一致性吗?
典型的方法是测量第90个百分点的延迟。这会在y轴上创建一个以毫秒为单位的折线图,您可以看到90%的事件比折线快。
我们知道100ms是快慢之间的感知阈值。
但是90ms的90ms 百分位延迟如何告诉我们我们的用户体验?不多。我们的应用程序在什么时间获得良好体验?没有办法确定。
93ms 的第90个百分位延迟如何告诉我们我们的用户体验?这可能比103ms的延迟要好一些,但这就是我们所能说的。同样,没有办法确定。
我们找到了解决此问题的方法: 测量100毫秒内事件的百分比。 此方法具有三大优势:
该指标围绕用户而定。它可以告诉我们我们的应用程序运行时间的百分比以及用户体验速度的百分比。
该度量标准使我们可以重新引入由于未测量到帧的最末端而损失的精度(第5节)。通过将目标设置为帧速率的倍数,接近目标的指标将落入较慢或较快的存储桶中。
该指标更易于计算。只需计算目标范围内的事件数,然后将其除以事件总数即可。百分位数更难计算。有一些有效的近似值,但是要正确执行此近似值,您需要计算每个度量。
这种方法只有一个缺点:如果您的速度慢于目标,则很难看到改进。
7.显示多个阈值
为了可视化性能优化的影响,我们在100ms之上和之下添加了其他阈值。我们将延迟分为<50毫秒(快速),<100毫秒(正常),<1000毫秒(慢)和“糟糕”。可怕的水桶让我们看到了严重缺失标记的地方,这就是为什么我们将其涂成鲜红色的原因。50ms的存储桶对变化非常敏感。性能改进通常在100ms桶之前就已经可见。
例如,此图直观地显示了在超人中查看线程的性能:
它显示了性能下降的时期,后来被修复。如果仅查看100ms结果(蓝色条的顶部),则很难发现回归。如果查看50ms的结果(绿色条的顶部),则很容易发现。
使用性能指标的传统方法,我们很可能已经错过了这个问题。但是由于我们如何衡量和可视化指标,我们能够非常迅速地发现并解决问题。
结论
性能指标要正确。
我们找到了一个更好的方法来为Web应用程序构建性能指标:
从event.timeStamp开始测量时间
测量时间结束于performance.now()在requestAnimationFrame()
忽略选项卡未聚焦时发生的任何事情
使用“未达到目标的事件百分比”汇总数据
可视化多个阈值
使用这种方法,您可以创建精确的性能指标。可以制作快速显示回归的图表。可以设计可视化,以快速显示优化的影响。