优化输入延迟

了解什么是输入延迟,并学习减少输入延迟以实现更快交互性的技巧。

Web 上的互动很复杂,浏览器中会发生各种活动来驱动它们。但它们的共同点是,在它们的事件回调开始运行之前,它们都会产生一些输入延迟。在本指南中,您将了解什么是输入延迟,以及您可以做些什么来最大限度地减少输入延迟,从而使您网站的互动运行得更快。

什么是输入延迟?

输入延迟是指从用户首次与页面互动(例如点击屏幕、用鼠标点击或按键)到互动事件回调开始运行之间的时间段。每次互动都始于一定量的输入延迟。

A simplified visualization of input delay. At left, there is line art of a mouse cursor with a starburst behind it, signifying the start of an interaction. To the right is line art of a cogwheel, signifying when the event handlers for an interaction begin to run. The space in between is noted as the input delay with a curly brace.
输入延迟背后的机制。当操作系统收到输入时,必须先将其传递给浏览器,然后互动才能开始。这需要一定的时间,并且可能会因现有的主线程工作而增加。

输入延迟的某些部分是不可避免的:操作系统始终需要一些时间来识别输入事件并将其传递给浏览器。但是,输入延迟的这一部分通常甚至不引人注意,并且页面本身发生的其他事情可能会使输入延迟足够长以致引起问题。

如何看待输入延迟

一般来说,您希望尽可能缩短交互的每个部分,以便您的网站有最大的机会满足 “交互到下次绘制”(INP) 指标的“良好”阈值,无论用户使用何种设备。控制输入延迟只是达到该阈值的一部分。

因此,您需要力求尽可能缩短输入延迟,以满足 INP 的“良好”阈值。但您应该意识到,您不能期望完全消除输入延迟。只要您在用户尝试与您的页面互动时避免过多的主线程工作,您的输入延迟就应该足够低,以避免问题。

如何最大限度地减少输入延迟

如前所述,某些输入延迟是不可避免的,但另一方面,某些输入延迟可以避免的。如果您正为长时间的输入延迟而苦恼,请考虑以下事项。

避免启动过多主线程工作的重复计时器

JavaScript 中有两种常用的计时器函数,它们可能会导致输入延迟:setTimeoutsetInterval。两者之间的区别在于 setTimeout 计划在指定时间后运行回调。setInterval 则计划每 n 毫秒永久运行一次回调,或者直到使用 clearInterval 停止计时器。

setTimeout 本身并没有问题——事实上,它有助于 避免长时间运行的任务。但是,这取决于超时何时发生,以及用户是否在超时回调运行时尝试与页面互动。

此外,setTimeout 可以在循环中或递归地运行,在这种情况下,它的行为更像 setInterval,尽管最好在完成前一次迭代后再计划下一次迭代。虽然这意味着每次调用 setTimeout 时循环都会让步于主线程,但您应该注意确保其回调最终不会执行过多的工作。

setInterval 按时间间隔运行回调,因此更有可能妨碍互动。这是因为——与单个 setTimeout 调用(这是一个一次性回调,可能会妨碍用户互动)不同——setInterval 的重复性质使其更有可能妨碍互动,从而增加互动的输入延迟。

A screenshot of the performance profiler in Chrome DevTools demonstrating input delay. A task fired by a timer function occurs just before a user initiates a click interaction. However, the timer extends the input delay, causing the interaction's event callbacks to run later than they otherwise would.
Chrome DevTools 性能面板中描述了先前 setInterval 调用注册的计时器导致输入延迟的情况。增加的输入延迟导致互动的事件回调运行时间晚于原本可能的时间。

如果计时器发生在第一方代码中,那么您可以控制它们。评估您是否需要它们,或者尽最大努力减少其中的工作。但是,第三方脚本中的计时器是另一回事。您通常无法控制第三方脚本的作用,而修复第三方代码中的性能问题通常需要与利益相关者合作,以确定给定的第三方脚本是否必要,如果必要,则与第三方脚本供应商建立联系,以确定可以采取哪些措施来修复它们可能在您的网站上造成的性能问题。

避免长时间运行的任务

缓解长时间输入延迟的一种方法是避免长时间运行的任务。当您有过多主线程工作在互动期间阻止主线程时,这会增加它们的输入延迟,直到长时间运行的任务有机会完成。

A visualization of how long tasks extend input delay. At top, an interaction occurs shortly after a single long task runs, causing significant input delay that causes event callbacks to run much later than they should. At bottom, an interaction occurs at roughly the same time, but the long task is broken up into several smaller ones by yielding, allowing the interaction's event callbacks to run much sooner.
当任务过长且浏览器无法足够快地响应互动时,互动会发生什么情况的可视化,以及当较长的任务被分解为较小的任务时的情况的可视化。

除了最大限度地减少您在任务中执行的工作量(您应该始终努力在主线程上尽可能少地工作)之外,您还可以通过 分解长时间运行的任务 来提高对用户输入的响应速度。

注意互动重叠

如果您有重叠的互动,那么优化 INP 的一个特别具有挑战性的部分可能是。互动重叠意味着在您与一个元素互动后,在初始互动有机会渲染下一帧之前,您会对页面进行另一次互动。

A depiction of when tasks can overlap to produce long input delays. In this depiction, a click interaction overlaps with a keydown interaction to increase the input delay for the keydown interaction.
Chrome DevTools 性能分析器中两个并发互动的可视化。初始点击互动中的渲染工作会导致后续键盘互动的输入延迟。

互动重叠的来源可能很简单,例如用户在短时间内进行多次互动。当用户在表单字段中键入时可能会发生这种情况,在这种情况下,在很短的时间内可能会发生多次键盘互动。如果按键事件的工作量特别大(例如在自动完成字段的常见情况下,其中对后端进行了网络请求),您有几个选择

  • 考虑 防抖 输入,以限制事件回调在给定时间段内执行的次数。
  • 使用 AbortController 取消传出的 fetch 请求,这样主线程就不会因处理 fetch 回调而变得拥塞。注意:AbortController 实例的 signal 属性也可用于 中止事件

由于互动重叠而导致输入延迟增加的另一个来源可能是昂贵的动画。特别是,JavaScript 中的动画可能会触发许多 requestAnimationFrame 调用,这可能会妨碍用户互动。为了解决这个问题,请尽可能使用 CSS 动画来避免排队可能昂贵的动画帧——但如果您这样做,请确保您 避免非合成动画,以便动画主要在 GPU 和合成器线程上运行,而不是在主线程上运行。

结论

虽然输入延迟可能不代表您的互动运行所需时间的大部分,但重要的是要理解,互动的每个部分都会占用您可以减少的时间量。如果您正在 观察到长时间的输入延迟,那么您就有机会减少它。避免重复的计时器回调、分解长时间运行的任务以及注意潜在的互动重叠都可以帮助您减少输入延迟,从而为您的网站用户带来更快的互动性。

题图来自 Unsplash,由 Erik Mclean 拍摄。