使用 isInputPending() 改善 JS 调度

这是一个新的 JavaScript API,可帮助您避免在加载性能和输入响应能力之间进行权衡。

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

快速加载很难。目前,利用 JS 呈现内容的网站必须在加载性能和输入响应速度之间进行权衡:要么一次性执行显示所需的所有工作(加载性能更好,输入响应速度较慢),要么将工作分解为更小的任务,以便对输入和绘制保持响应能力(加载性能较差,输入响应速度较快)。

为了避免进行这种权衡,Facebook 在 Chromium 中提议并实现了 isInputPending() API,以便在不让出的情况下提高响应速度。根据来源试用反馈,我们对该 API 进行了多项更新,现在很高兴地宣布,该 API 现在默认在 Chromium 87 中提供!

浏览器兼容性

浏览器支持

  • Chrome:87.
  • Edge:87.
  • Firefox:不受支持。
  • Safari:不受支持。

来源

isInputPending() 从 87 版开始在基于 Chromium 的浏览器中提供。 没有其他浏览器发出过要发布该 API 的意图。

背景

当今 JS 生态系统中的大多数工作都是在单个线程(即主线程)上完成的。这为开发者提供了强大的执行模型,但如果脚本执行时间过长,用户体验(尤其是响应速度)可能会受到严重影响。例如,如果网页在触发输入事件时正在执行大量工作,则该网页将在完成这些工作后才处理点击输入事件。

目前的最佳实践是将 JavaScript 拆分为更小的块来解决此问题。在网页加载期间,网页可以运行一些 JavaScript,然后让出并将控制权传回给浏览器。然后,浏览器可以检查其输入事件队列,看看是否有任何需要告知网页的内容。然后,浏览器就可以在添加 JavaScript 代码块后继续运行这些代码块。这有助于解决问题,但可能会导致其他问题。

每当网页将控制权交还给浏览器时,浏览器都需要花费一些时间来检查其输入事件队列、处理事件并接收下一个 JavaScript 代码块。虽然浏览器会更快地响应事件,但网页的总体加载时间会变慢。如果我们让出太多时间,页面加载速度就会太慢。如果我们减少让步的频率,浏览器响应用户事件所需的时间就会延长,用户会感到沮丧。没意思。

一张示意图,显示当您运行长时间运行的 JS 任务时,浏览器有更少的时间来调度事件。

在 Facebook 上,我们希望能设计出一种新的加载方法来消除这种令人沮丧的权衡。为此,我们联系了 Chrome 团队的朋友,并提出了 isInputPending() 方案。isInputPending() API 是第一个针对 Web 上的用户输入使用中断概念的 API,它允许 JavaScript 在无需让出浏览器的情况下检查输入。

一张示意图,显示 isInputPending() 可让 JS 检查是否有待处理的用户输入,而无需完全将执行权交还给浏览器。

由于该 API 受到了业界的关注,我们与 Chrome 团队的同事合作,在 Chromium 中实现并发布了该功能。在 Chrome 工程师的帮助下,我们在源试用(一种在 Chrome 完全发布 API 之前测试更改并从开发者那里获取反馈的方式)之后发布了补丁。

我们现已采纳了来自原始试用和 W3C Web 性能工作组其他成员的反馈,并对 API 进行了更改。

示例:让出更多时间的调度程序

假设您需要执行大量会阻塞显示的工作来加载网页,例如从组件生成标记、提取素数,或者只是绘制一个酷炫的加载旋转图标。其中每项都分解为单独的工作项。使用调度程序模式,我们来大致了解一下如何在假设的 processWorkQueue() 函数中处理工作:

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

通过在稍后通过 setTimeout() 在新的宏任务中调用 processWorkQueue(),我们可以让浏览器能够对输入做出一定程度的响应(它可以在工作恢复之前运行事件处理脚本),同时仍能相对不间断地运行。不过,我们可能会被想要控制事件循环的其他工作取消调度很长时间,或者会额外增加 QUANTUM 毫秒的事件延迟时间。

这还不错,但能不能做得更好?当然可以!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

通过引入对 navigator.scheduling.isInputPending() 的调用,我们能够更快地响应输入,同时确保我们的显示屏阻塞工作能够不间断地执行。如果我们不想在工作完成之前处理输入(例如绘制)以外的任何内容,也可以轻松增加 QUANTUM 的长度。

默认情况下,isInputPending() 不会返回“连续”事件。这些值包括 mousemovepointermove 等。如果您也对这些内容感兴趣,那就没问题。只需向 isInputPending() 提供一个 includeContinuous 设置为 true 的对象,即可顺利运行:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

大功告成!React 等框架正在使用类似的逻辑在其核心调度库中构建 isInputPending() 支持。希望这样可以让使用这些框架的开发者能够在后台受益于 isInputPending(),而无需进行大量重写。

让步并不总是坏事

值得注意的是,减少生成的结果并非适用于所有用例的正确解决方案。除了处理输入事件之外,返回控制权给浏览器还有许多原因,例如在网页上执行呈现和执行其他脚本。

在某些情况下,浏览器无法正确归因待处理的输入事件。特别是,为跨源 iframe 设置复杂的剪辑和遮罩可能会报告假负例(即,isInputPending() 在定位到这些框架时可能会意外返回 false)。如果您的网站确实需要与样式化的子框架进行互动,请务必足够频繁地让出资源。

同时也要注意共享事件循环的其他网页。在 Android 版 Chrome 等平台上,多个源共用一个事件循环是很常见的。如果输入被调度到跨源框架,isInputPending() 将永远不会返回 true,因此后台页面可能会干扰前台页面的响应能力。使用 Page Visibility API 在后台执行工作时,您可能需要更频繁地减少、推迟或让出工作。

我们建议您谨慎使用 isInputPending()。如果没有要执行的会阻塞用户的工作,请多让出事件循环,以便其他任务顺利运行。长任务可能会有害

反馈

总结

我们很高兴 isInputPending() 即将发布,开发者可以立即开始使用它。这是 Facebook 首次构建新的 Web API,并将其从概念孵化阶段推进到标准提案阶段,最终在浏览器中实际发布。我们衷心感谢所有帮助我们走到今天这一步的各方人士,并特别感谢 Chrome 团队中帮助我们完善此想法并将其顺利发布的所有人!

主打照片由 Will H McMahan 拍摄,选自 Unsplash