有句话说:“无法衡量,就无法改善。” 这篇文章的作者(我们今天要翻译的译本)是为
Superhuman工作的 。 他说,这家公司正在开发世界上最快的电子邮件客户端。 在这里,我们将讨论什么是“快速”,以及如何创建工具来测量难以置信的快速Web应用程序的性能。

应用速度测量
为了改善我们的发展,我们花费了大量时间来衡量其速度。 而且,事实证明,绩效指标是难以理解和应用的指标。
一方面,很难设计能够准确描述用户在使用系统时所经历的感觉的度量。 另一方面,要创建如此精确的指标以至于它们的分析使您能够做出明智的决定并不容易。 结果,许多开发团队无法信任他们收集的有关项目绩效的数据。
即使开发人员拥有可靠和准确的指标,使用它们也不容易。 如何定义“快速”一词? 如何在速度和一致性之间找到平衡? 如何学习快速检测性能下降或学习评估优化对系统的影响?
在这里,我们想分享一些有关Web应用程序性能分析工具开发的想法。
1.使用正确的“时钟”
JavaScript有两种检索时间戳的机制:
performance.now()
和
new Date()
。
它们有何不同? 以下两个差异对我们至关重要:
performance.now()
方法更加准确。 new Date()
构造的精度为±1 ms,而performance.now()
的精度已经为±100 µs(是的,大约是微秒 !)。- 由
performance.now()
方法返回的值始终以恒定速率增加,并且与系统时间无关。 此方法仅测量时间间隔,而无需关注系统时间。 并在new Date()
影响系统时间。 如果重新安排系统时钟,它还将更改new Date ()
返回的值,这将破坏性能监视数据。
尽管用
performance.now()
方法表示的“时钟”显然更适合于测量时间间隔,但它们也不理想。
performance.now()
和
new Date()
遇到相同的问题,这在系统处于睡眠状态时会表现出来:测量包括机器甚至不处于活动状态的时间。
2.检查应用程序活动
如果您要衡量Web应用程序的性能,请从其选项卡切换到其他选项卡-这将破坏数据收集过程。 怎么了 事实是浏览器限制了位于后台选项卡中的应用程序。
在两种情况下,指标可能会失真。 结果,该应用程序看起来比实际速度要慢得多。
- 电脑进入睡眠模式。
- 该应用程序在浏览器的后台选项卡中运行。
这两种情况的发生并不罕见。 幸运的是,我们有两种解决方案。
首先,我们可以简单地忽略失真的指标,丢弃与某些合理值相差太大的测量结果。 例如,按下按钮时调用的代码根本无法执行15分钟! 也许这是您唯一需要解决的两个问题。
其次,可以使用
document.hidden
属性和
visiblechange事件。 当用户从感兴趣的浏览器选项卡切换到另一个选项卡或返回到我们感兴趣的选项卡时,会引发
visibilitychange
更改事件。 当计算机开始工作并退出睡眠模式时,浏览器窗口最小化或最大化时,将调用此方法。 换句话说,这正是我们所需要的。 另外,只要选项卡在后台,
document.hidden
属性为
true
。
这是一个简单示例,演示了
document.hidden
属性和
visibilitychange
事件的使用。
let lastVisibilityChange = 0 window.addEventListener('visibilitychange', () => { lastVisibilityChange = performance.now() }) // , , // , , if (metric.start < lastVisibilityChange || document.hidden) return
如您所见,我们丢弃了一些数据,但这很好。 事实是,当程序无法完全使用系统资源时,这些数据与程序的那些时期有关。
现在我们讨论了我们不感兴趣的指标。 但是在很多情况下,收集的数据对我们来说非常有趣。 让我们看看如何收集这些数据。
3.搜索指示器,以使您能够最好地捕获事件开始的时间
JavaScript最具争议的功能之一是该语言的事件循环是单线程的。 在某个时间点,只有一条代码能够执行,其执行不能中断。
如果用户在执行某个代码时按下按钮,则在完成该代码的执行之前,程序将不知道该按钮。 例如,如果应用程序在一个连续周期中花费了1000毫秒,并且用户在该周期开始后100毫秒按下了
Escape
按钮,则该事件将不会再记录900毫秒。
这会严重扭曲指标。 如果我们需要准确地测量用户对程序的理解程度,那么这将是一个巨大的问题!
幸运的是,解决这个问题并不是那么困难。 如果我们正在谈论当前事件,那么可以使用
window.event.timeStamp
(创建事件的时间),而不是使用
performance.now()
(看到事件的时间)。
事件的时间戳由主浏览器进程设置。 由于锁定JS事件循环时此过程不会阻塞,因此
event.timeStamp
为我们提供了有关实际触发事件的更多有价值的信息。
应当指出,这种机制不是理想的。 因此,从按下物理按钮到相应事件到达Chrome的那一刻之间,经过了9到15毫秒的无法解释的时间(
这是一篇很棒的文章,您可以从中了解发生这种情况的原因)。
但是,即使我们可以衡量事件到达Chrome所需的时间,我们也不应将此时间纳入指标。 怎么了 事实是,我们无法将这样的优化引入可能严重影响此类延迟的代码中。 我们无法以任何方式改进它们。
结果,如果我们谈论寻找事件开始的时间戳,那么
event.timeStamp
指示器在这里看起来最合适。
事件何时结束的最佳估计是什么?
4.关闭requestAnimationFrame()中的计时器
JavaScript中事件循环设备的功能还带来了另外一个后果:某些与您的代码无关的代码可以在它之后但在浏览器在屏幕上显示页面的更新版本之前执行。
考虑一下React。 执行代码后,React将更新DOM。 如果仅在代码中测量时间,则意味着您将不会测量执行React代码所花费的时间。
为了测量这额外的时间,我们使用
requestAnimationFrame()
关闭计时器。 仅当浏览器准备输出下一帧时,才执行此操作。
requestAnimationFrame(() => { metric.finish(performance.now()) })
这是框架的生命周期(该图取自
requestAnimationFrame
上的
这一奇妙材料)。
框架生命周期如该图所示,在处理器完成之后,即在显示框架之前,将调用
requestAnimationFrame()
。 如果我们在此处关闭计时器,则意味着我们可以完全确定,花费时间刷新屏幕的所有内容都包含在该时间间隔内收集的数据中。
到目前为止还不错,但是现在情况变得相当复杂...
5.忽略创建页面布局及其可视化所需的时间。
上图显示了框架的生命周期,它说明了我们遇到的另一个问题。 在框架生命周期的最后,有版式块(形成页面布局)和画图(显示页面)。 如果您不考虑完成这些操作所需的时间,那么我们测量的时间将少于一些更新数据出现在屏幕上的时间。
幸运的是,
requestAnimationFrame
还有另外一个王牌。 调用由
requestAnimationFrame
传递的函数时,将为该函数传递一个时间戳,该时间戳指示当前帧形成的开始时间(即,位于图的最左侧的那一帧)。 该时间戳通常非常接近上一帧的结束时间。
结果,可以通过测量从
event.timeStamp
时刻到形成下一帧的时间所经过的总时间来纠正上述缺陷。 注意嵌套的
requestAnimationFrame
:
requestAnimationFrame(() => { requestAnimationFrame((timestamp) => { metric.finish(timestamp) }) })
尽管上面显示的内容看起来是解决该问题的绝佳方法,但最终,我们决定不使用此设计。 事实是,尽管该技术使得获得更可靠的数据成为可能,但是这种数据的准确性降低了。 Chrome中的帧以16毫秒的频率形成。 这意味着我们可以使用的最高精度为±16 ms。 而且,如果浏览器过载并跳过帧,那么准确性将更低,并且这种恶化将是不可预测的。
如果实施此解决方案,则代码性能的重大提高(例如,将之前执行的任务加速到32 ms,最高可达15 ms)可能不会影响性能测量结果。
不考虑创建页面布局及其输出所需的时间,我们得到了我们控制下的代码的更准确指标(±100μs)。 结果,我们可以获得对该代码所做的任何改进的数值表达式。
我们还探索了类似的想法:
requestAnimationFrame(() => { setTimeout(() => { metric.finish(performance.now()) } })
这将包括渲染时间,但是指示器的精度将不限于±16 ms。 但是,我们决定也不使用这种方法。 如果系统遇到长输入事件,则在更新用户界面之后,对
setTimeout
传输的调用可能会大大延迟并执行。
6.澄清“低于目标的事件所占百分比”
我们正在开发一个项目,并将重点放在高性能上,尝试通过两种方式对其进行优化:
- 速度 最快的任务的执行时间应尽可能接近0 ms。
- 均匀性 最慢的任务的执行时间应尽可能接近最快的任务的执行时间。
由于这些指标会随时间而变化,因此它们很难可视化,也不容易讨论。 是否有可能建立一个可视化的指标体系,以激励我们优化速度和均匀性?
一种典型的方法是测量延迟的90%。 这种方法使您可以沿Y轴绘制折线图,该线可以节省时间(以毫秒为单位)。 此图使您可以看到90%的事件都在折线图下方,也就是说,它们的执行速度比折线图指示的时间快。
众所周知,
100 ms是所谓的“快”和“慢”之间的界限。
但是,如果我们知道延迟的第90个百分位数是103 ms,那么我们将如何发现用户的工作感觉呢? 不是特别多。 哪些指标将为用户提供可用性? 无法肯定地知道这一点。
但是,如果我们知道延迟的第90个百分位数是93 ms,该怎么办? 有人认为93优于103,但我们无法就这些指标以及它们对用户对项目的理解表示什么。 同样,这个问题没有确切答案。
我们找到了解决该问题的方法。 它包括测量执行时间不超过100 ms的事件的百分比。 这种方法有三大优点:
- 该指标是面向用户的。 她可以告诉我们我们的应用程序有多快的时间百分比,以及有多少百分比的用户将其视为快速。
- 该度量标准使我们能够将测量结果恢复到由于没有在帧的最后端完成任务所花费的时间而丢失的精度(我们在第5节中讨论了这一点)。 由于我们将目标指标设置为适合多个框架的事实,因此接近该指标的测量结果可能小于或大于该指标。
- 此指标更易于计算。 只需计算执行时间低于目标指标的事件数,然后将其除以事件总数即可。 百分位数要难得多。 有有效的近似值,但是为了正确处理所有事情,您需要考虑每个维度。
这种方法只有一个缺点:如果指标比目标差,那么将很难注意到它们的改进。
7.在指标分析中使用几个阈值
为了可视化性能优化的结果,我们在系统中引入了多个附加阈值-100 ms以上及以下。
我们将延迟分为以下几类:
- 少于50毫秒(快速)。
- 50到100毫秒(良好)。
- 100至1000毫秒(慢)。
- 超过1000毫秒(非常慢)。
“非常慢”的结果使我们看到我们非常错过某个地方。 因此,我们用鲜红色突出显示它们。
50毫秒内的变化对变化非常敏感。 在这里,性能改进通常在与100毫秒对应的组中很久之前就可见。
例如,下图直观显示了超人中线程查看的性能。
查看线程它显示了性能下降的时期,然后-改善的结果。 如果仅查看与100毫秒(蓝色列的上部)相对应的指示器,则很难评估性能下降。 查看适合50毫秒(绿色列的上部)的结果时,性能问题已经很清楚地看到了。
如果我们使用传统方法来研究性能指标,则可能不会注意到上图中显示的问题,该问题对系统的影响。 但是,由于我们进行测量的方式以及对指标进行可视化的方式,我们能够非常快速地发现并解决问题。
总结
事实证明,找到正确的方法来处理性能指标非常困难。 我们设法开发出一种方法,使我们能够创建用于测量Web应用程序性能的高质量工具。 即,我们正在谈论以下内容:
- 使用
event.timeStamp
测量事件的开始时间。 - 事件结束时间是使用传递给
requestAnimationFrame()
的回调中的performance.now()
来衡量的。 - 当应用程序处于非活动浏览器选项卡上时,该应用程序发生的所有事情都将被忽略。
- 数据使用指标进行汇总,指标可以描述为“低于目标的事件百分比”。
- 数据可以通过几个级别的阈值可视化。
该技术为您提供了创建可靠和准确指标的工具。 您可以构建清楚地表明性能下降的图表,还可以可视化优化结果。 最重要的是-您有机会更快地进行快速项目。
亲爱的读者们! 您如何分析Web应用程序的性能?
