使用 isInputPending() 改善 JS 调度

新增了一个 JavaScript API,可能有助于您在加载性能与输入响应能力之间权衡取舍。

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

快速加载并非易事。目前,利用 JS 渲染内容的网站必须在加载性能与输入响应能力之间进行权衡:要么一次性执行显示所需的全部工作(更好的加载性能,更差的输入响应速度),要么将工作分成较小的任务,以保持对输入和绘制的响应(加载性能较差,输入响应速度较慢)。

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

浏览器兼容性

浏览器支持

  • 87
  • 87
  • x
  • x

从版本 87 开始,基于 Chromium 的浏览器才提供 isInputPending()。 任何其他浏览器都未发出过发送此 API 的意图。

背景

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

当前的最佳做法是将 JavaScript 拆分为更小的块来处理此问题。网页加载时,网页可以运行一些 JavaScript,然后将控制权交还给浏览器。然后,浏览器可以检查其输入事件队列,并查看是否需要告知网页的任何内容。这样,在添加 JavaScript 块时,浏览器就可以继续运行。这会有帮助,但可能会导致其他问题。

每当网页将控制权交还给浏览器时,浏览器都需要一些时间来检查其输入事件队列、处理事件并选取下一个 JavaScript 块。虽然浏览器响应事件速度更快,但页面的总体加载时间会变慢。一旦出价过高 网页加载速度就会过慢如果我们降低频率,浏览器需要更长时间才能响应用户事件,这会让用户感到失望。没意思。

此图展示了运行较长的 JS 任务时,浏览器调度事件所需的时间更少。

在 Facebook,我们想要看看,如果我们想出一种新的加载方法,从而消除这种令人沮丧的权衡,结果会是怎样的。我们联系了 Chrome 的好友,并提出了针对 isInputPending() 的提案。isInputPending() API 是第一个采用中断概念在网络上进行用户输入的 API,该 API 可让 JavaScript 在不让出浏览器的情况下检查输入信息。

一张显示 isInputPending() 的示意图,可以让您的 JS 检查是否有待处理的用户输入,而不会完全让执行让浏览器执行。

由于用户对该 API 感兴趣,我们与 Chrome 的同事合作,在 Chromium 中实现了并推出该功能。在 Chrome 工程师的帮助下,我们获得了这些补丁,并进行的是源试用(这是 Chrome 可以在全面发布 API 之前测试更改并获取开发者反馈的一种方式)。

现在,我们已从源试用和 W3C Web 性能工作组的其他成员那里收集了反馈,并对该 API 进行了更改。

示例:Yieldier 调度器

假设您要加载网页时需要执行许多会阻塞显示的工作,例如从组件生成标记、找出质询,或者只是绘制一个炫酷的加载旋转图标。其中每个属性被分解为一个离散的工作项。使用调度器模式,让我们大致了解如何在假设的 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)。如果您的网站确实需要与风格化的子框架进行互动,请确保您的收益足够高。

另外,也请注意共享事件循环的其他网页。在 Chrome(Android 版)等平台上,多个源共享事件循环是很常见的。如果将输入分派给跨源帧,isInputPending() 将永远不会返回 true,因此后台网页可能会干扰前台网页的响应能力。使用 Page Visibility API 在后台运行时,您可能希望减少、推迟或更频繁地产出。

我们建议您慎重使用 isInputPending()。如果没有会妨碍用户完成的工作,则应提高让步频率,善待事件循环中的其他人。耗时较长的任务可能会带来负面影响

反馈

  • 请在 is-input-pending 代码库中提供有关该规范的反馈。
  • 在 Twitter 上联系 @acomminos(规范作者之一)。

总结

我们很高兴 isInputPending() 即将发布,开发者也能够立即开始使用它。此 API 是 Facebook 首次构建新的 Web API,并将其从创意孵化到标准提案,再到实际在浏览器中发布。我们衷心感谢所有帮助我们实现这一目标的每一位 Chrome 员工,并特别感谢所有帮助我们完善这一想法并最终将其付诸实施的 Chrome 团队!

主打照片由 Will H McMahan 拍摄,来自 Unsplash 用户。