使用 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