对齐的输入事件

Dave Tapuska
Dave Tapuska

要点

  • Chrome 60 通过降低事件频率来减少卡顿,从而提高帧时间的一致性。
  • Chrome 58 中引入的 getCoalescedEvents() 方法可提供与您一直以来获得的丰富事件信息相同的信息。

提供流畅的用户体验对网站而言至关重要。从收到输入事件到视觉效果实际更新之间的时间非常重要,通常,减少工作量非常重要。在过去几次 Chrome 版本的发布中,我们缩短了这些设备上的输入延迟时间。

为了确保顺畅度和性能,我们在 Chrome 60 中进行了一项更改,使这些事件的发生频率降低,同时提高了所提供信息的精细程度。就像在 Jelly Bean 发布并引入了用于在 Android 上对齐输入的 Choreographer 一样,我们将在所有平台上为 Web 引入帧对齐输入。

但有时,您需要更多事件。因此,在 Chrome 58 中,我们实现了一个名为 getCoalescedEvents() 的方法,让您的应用即使在接收较少事件时,也能检索指针的完整路径。

我们先来谈一谈事件频次。

降低事件频率

我们先来了解一些基本知识:触摸屏以 60-120Hz 的速率提供输入,而鼠标通常以 100Hz 的速率提供输入(但最高可达 2000Hz)。但显示器的典型刷新率为 60Hz。这到底是什么意思?这意味着,我们接收输入的速率高于实际更新显示屏的速率。接下来,我们来看看 Chrome 开发者工具中一个简单的画布绘制应用的性能时间轴。

在下图中,停用 requestAnimationFrame() 对齐输入后,您可以看到每帧有多个处理块,并且帧时间不一致。黄色小方块表示对 DOM 事件的目标、调度事件、运行 JavaScript、更新悬停的节点以及可能重新计算布局和样式等进行的点击测试。

显示帧时间不一致的性能时间轴

那么,为什么我们要做这些不会导致任何视觉更新的额外工作呢?理想情况下,我们不希望执行任何最终不会给用户带来好处的工作。从 Chrome 60 开始,输入流水线将延迟调度连续事件(wheelmousewheeltouchmovepointermovemousemove),并在 requestAnimationFrame() 回调发生之前调度这些事件。在下图(已启用该功能)中,您会看到帧时间更稳定,处理事件所需的时间更短。

我们一直在 Canary 和 Dev 渠道上运行一项实验,在启用此功能后,发现命中测试次数减少了 35%,这让主线程可以更频繁地运行。

Web 开发者应注意的重要说明是,发生的任何离散事件(例如 keydownkeyupmouseupmousedowntouchstarttouchend)都会立即与所有待处理事件一起调度,并保留相对顺序。启用此功能后,系统会将许多工作简化到正常的事件循环流中,从而提供一致的输入间隔。这样一来,连续事件便与 scrollresize 事件保持一致,而这些事件已被简化为 Chrome 中的事件循环流程。

显示相对一致的帧时间的性能时间轴。

我们发现,使用此类事件的绝大多数应用都不需要更高的频率。Android 已经多年在使用一致的事件,因此没有任何新变化,但网站在桌面平台上获得的事件可能不太精细。主线程卡顿一直是导致输入流畅度出现问题的一个问题,这意味着,每当应用执行工作时,您都可能会看到位置跳跃,因此无法知道指针是如何从一个位置跳转到另一个位置的。

getCoalescedEvents() 方法

正如我所说,在极少数情况下,应用更希望知道指针的完整路径。因此,为了解决您看到较大跳跃和事件频率降低的情况,我们在 Chrome 58 中推出了指针事件扩展程序 getCoalescedEvents()。下面的示例展示了如果您使用此 API,如何将主线程上的卡顿隐藏起来。

比较标准事件和合并事件。

您可以访问导致该事件的历史事件数组,而不是接收单个事件。AndroidiOSWindows 的原生 SDK 中都包含非常相似的 API,我们也将向 Web 公开类似的 API。

通常,绘图应用可能会通过查看事件的偏移量来绘制点:

window.addEventListener("pointermove", function(event) {
    drawPoint(event.pageX, event.pageY);
});

此代码可以轻松更改为使用事件数组:

window.addEventListener("pointermove", function(event) {
    var events = 'getCoalescedEvents' in event ? event.getCoalescedEvents() : [event];
    for (let e of events) {
    drawPoint(e.pageX, e.pageY);
    }
});

请注意,并非合并事件中的每个属性都会填充。由于合并的事件实际上并未分派,而是随之而来,因此不会进行命中测试。某些字段(例如 currentTargeteventPhase)将具有默认值。调用与调度相关的方法(例如 stopPropagation()preventDefault())对父事件没有影响。