对齐的输入事件

戴夫·塔普斯卡
Dave Tapuska

要点

  • Chrome 60 可通过降低事件频率来减少卡顿,从而改善帧时间的一致性。
  • Chrome 58 中引入的 getCoalescedEvents() 方法可提供您一直以来已有的大量事件信息。

提供流畅的用户体验对网站来说非常重要。收到输入事件与视觉内容实际更新之间的时间很重要,通常执行较少的工作也很重要。在过去的几个 Chrome 版本中,我们降低了这些设备之间的输入延迟。

为保证流畅性和性能,我们在 Chrome 60 中进行了一项更改,使这些事件发生的频率降低,同时增加所提供信息的粒度。与发布 Jelly Bean 并推出可在 Android 上对齐输入的 Choreographer 时一样,我们将在所有平台上引入与帧对齐的输入。

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

我们先来谈谈事件频率。

降低事件频率

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

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

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

那么,我们为什么要进行不会产生任何视觉更新的额外工作呢?理想情况下,我们不想执行任何不会最终让用户受益的工作。从 Chrome 60 开始,输入流水线会延迟分派连续事件(wheelmousewheeltouchmovepointermovemousemove)的时间,并会在发生 requestAnimationFrame() 回调之前分派这些事件。在下图(启用了该功能)中,您可以看到帧时间更加一致,处理事件的时间缩短了。

我们一直在运行一项实验,在 Canary 版和开发者版中启用此功能后,我们发现执行的点击测试减少了 35%,这让主线程能够更频繁地运行。

Web 开发者需要注意的一点是,发生的任何离散事件(例如 keydownkeyupmouseupmousedowntouchstarttouchend)都会与所有待处理事件一起立即分派,并保持相对排序。启用此功能后,大量工作都可简化到常规的事件循环流程中,从而提供一致的输入间隔。这样一来,连续事件内嵌到 scrollresize 事件中,而这些事件已经简化为 Chrome 中的事件循环流程。

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

我们发现,绝大多数使用此类事件的应用都没有更高的频率。多年来,Android 已经对事件进行了调整,因此没有什么新变化,但网站在桌面平台上遇到的事件可能不太精细。主线程卡顿一直存在一个问题,因此会导致输入流畅性出现中断,这意味着,只要应用执行工作,您就会看到位置跳跃,从而无法知道指针是如何从一个位置到达另一个位置的过程。

getCoalescedEvents() 方法

如前所述,在极少数情况下,应用更希望知道指针的完整路径。因此,为了解决出现大量跳跃和事件频率下降的问题,我们在 Chrome 58 中发布了一个名为 getCoalescedEvents() 的指针事件扩展程序。以下示例展示了在您使用此 API 时,如何在应用中隐藏主线程上的卡顿。

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

您可以访问导致相应事件的历史事件数组,而不是接收单个事件。AndroidiOSWindows 的原生 SDK 中都有非常相似的 API,并且我们将向网页提供类似的 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() 等与调度相关的方法对父事件没有影响。