推出 scheduler.yield 源试用

打造能够快速响应用户输入的网站一直是 Web 性能最具挑战性的方面之一,Chrome 团队一直在努力帮助 Web 开发者实现这一目标。就在今年,我们宣布Interaction to Next Paint (INP) 指标将从实验性指标转为待处理指标。现在,我们将于 2024 年 3 月取代 First Input Delay (FID),成为 Core Web Vitals 指标。

为了持续提供有助于 Web 开发者尽可能提高网站运行速度的新 API,Chrome 团队目前正在开展scheduler.yield 源试用,从 Chrome 115 版开始。scheduler.yield 是调度程序 API 中提议的新功能,与传统上依赖的方法相比,它提供了一种更简单、更有效的方式来将控制权让渡回主线程。

在让出时

JavaScript 使用“运行到完成”模式来处理任务。这意味着,当任务在主线程上运行时,该任务会在必要时运行,直到完成。任务完成后,控制权将交还给主线程,以便主线程处理队列中的下一个任务。

除了任务永远无法完成的极端情况(例如无限循环)之外,让出是 JavaScript 任务调度逻辑中不可避免的方面。这类问题一定会发生,只是时间早晚的问题,因此越早解决越好。如果任务运行时间过长(确切而言,超过 50 毫秒),则会被视为耗时较长的任务

冗长任务是导致网页响应速度缓慢的一个原因,因为它们会延迟浏览器响应用户输入的能力。长时间运行的任务越频繁,运行时间越长,用户就越有可能认为网页运行缓慢,甚至认为网页完全崩溃。

不过,即使您的代码在浏览器中启动了任务,也不意味着您必须等到该任务完成后,控制权才会交还给主线程。您可以通过在任务中明确让出,提高对网页上用户输入的响应速度,这会将任务拆分为在下次有机会时完成的任务。这样,其他任务就可以更快地在主线程上获得时间,而无需等待长时间运行的任务完成。

一张图片,展示了拆分任务如何有助于提高输入响应速度。在顶部,长任务会阻止事件处理脚本运行,直到任务完成。在底部,分块任务允许事件处理程序比其运行得更早运行。
可视化地展示了将控制权让渡回主线程的过程。首先,让出发生在任务运行完成之后,这意味着任务可能需要更长时间才能完成,然后再将控制权返还给主线程。归根结底,系统会显式地进行让出,将一个长任务拆分为多个较小的任务。这样,用户互动就可以更早运行,从而提高输入响应速度和 INP。

显式让出时,您是在告诉浏览器:“嘿,我知道我要执行的工作可能需要一段时间,我不希望您必须先完成所有工作,然后才能响应用户输入或执行其他可能也很重要的任务。”它是开发者工具箱中的一种宝贵工具,对改善用户体验大有帮助。

当前收益策略存在的问题

常见的产生方法会使用超时值为 0setTimeout。之所以能这样做,是因为传递给 setTimeout 的回调会将剩余工作移至单独的任务,该任务将加入队列以供后续执行。您不是等待浏览器自行让出,而是说“让我们将这项大工作拆分成更小的部分”。

不过,使用 setTimeout 让出会产生一个可能不希望的副作用:在让出点之后执行的工作将移至任务队列的后面。由用户互动调度的任务仍会按预期移至队列前面,但您在明确让出后想要执行的剩余工作最终可能会因排在前面的来自竞争来源的其他任务而进一步延迟。

如需了解此功能的实际运作方式,请试用此 Glitch 演示,或在下面嵌入的版本中进行实验。该演示包含一些您可以点击的按钮,以及这些按钮下方的框(用于记录任务的运行时间)。进入该页面后,请执行以下操作:

  1. 点击标有定期运行任务的顶部按钮,这将安排阻止任务每隔一段时间运行一次。点击此按钮后,任务日志中会填充多条消息,其中会显示 Ran blocking task with setInterval
  2. 接下来,点击标签为运行循环,并在每次迭代中使用 setTimeout 进行求值的按钮。

您会发现,演示底部的框中会显示如下内容:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

此输出演示了使用 setTimeout 让出时发生的“任务队列结束”行为。运行的循环会处理五个项,并在处理完每个项后使用 setTimeout 进行 yield。

这说明了网络上的一个常见问题:脚本(尤其是第三方脚本)注册按一定间隔运行工作的计时器函数的情况很常见。使用 setTimeout 让出时出现的“任务队列末尾”行为意味着,来自其他任务来源的工作可能会排在循环在让出后必须执行的剩余工作之前。

这可能或许不是理想的结果,具体取决于您的应用;但在许多情况下,正是由于这种行为,开发者可能不愿意轻易放弃对主线程的控制。让出主线程有好处,因为用户互动有机会更早运行,但这也让其他非用户互动工作也有机会在主线程上运行。这是一个真正的问题,但 scheduler.yield 可以帮助您解决此问题!

输入 scheduler.yield

从 Chrome 115 版开始,scheduler.yield 作为实验性 Web 平台功能通过标志提供。您可能会问:“为什么我需要使用特殊函数来产生 yield,而 setTimeout 已经可以执行此操作?”

值得注意的是,让线程让出 CPU 并不是 setTimeout 的设计目标,而是在安排回调在未来的某个时间点运行时产生的一个良好副作用,即使指定了 0 的超时值也是如此。不过,更重要的是,使用 setTimeout 让出时,系统会将剩余工作发送到任务队列的后面。默认情况下,scheduler.yield 会将剩余工作发送到队列的前面。这意味着,您希望在让出后立即继续的工作不会妨碍其他来源的任务(值得注意的是用户互动除外)。

scheduler.yield 是一个函数,会让出主线程,并在被调用时返回 Promise。这意味着您可以在 async 函数中对其执行 await 操作:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

如需查看 scheduler.yield 的实际效果,请执行以下操作:

  1. 导航到 chrome://flags
  2. 启用实验性 Web 平台功能实验。执行此操作后,您可能需要重启 Chrome。
  3. 前往演示页面,或使用此列表下方的嵌入式版本。
  4. 点击顶部的标签为定期运行任务的按钮。
  5. 最后,点击标签为 Run loop, deliverying with scheduler.yield on 每次迭代的按钮。

页面底部框中的输出结果将如下所示:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

与使用 setTimeout 产生 yield 的演示不同,您可以看到,即使循环在每次迭代后产生 yield,也不会将剩余工作发送到队列的后面,而是发送到队列的前面。这样,您既可以让步以提高网站上的输入响应速度,又可以确保您希望在让步完成的工作不会延迟。

快来试试吧!

如果您对 scheduler.yield 感兴趣并想试用,从 Chrome 115 版开始,您可以通过以下两种方式进行试用:

  1. 如果您想在本地实验 scheduler.yield,请在 Chrome 的地址栏中输入 chrome://flags,然后从实验性 Web 平台功能部分的下拉菜单中选择启用这样一来,scheduler.yield(以及任何其他实验性功能)将仅在您的 Chrome 实例中可用。
  2. 如果您想为公开可访问的来源中的真实 Chromium 用户启用 scheduler.yield,则需要注册 scheduler.yield 源试用。这样,您就可以在给定的时间段内安全地试用建议的功能,并为 Chrome 团队提供有关这些功能在实际中使用方式的宝贵洞见。如需详细了解来源试用版的运作方式,请参阅这份指南

如何使用 scheduler.yield(同时支持未实现 scheduler.yield 的浏览器)取决于您的目标。您可以使用官方 polyfill。如果您遇到以下情况,该 polyfill 会很有用:

  1. 您已经在应用中使用 scheduler.postTask 来安排任务。
  2. 您希望能够设置任务和让出优先级。
  3. 您希望能够通过 scheduler.postTask API 提供的 TaskController 类取消或重新排定任务的优先级。

如果您的情况与上述情况不符,则该兼容性插件可能不适合您。在这种情况下,您可以通过几种方式来发布自己的后备广告。第一种方法使用 scheduler.yield(如果可用),但如果不可用,则回退到 setTimeout

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

这样做可以行,但您可能猜到,不支持 scheduler.yield 的浏览器将没有“队列前”行为。如果这意味着您完全不想让出,您可以尝试另一种方法,这种方法使用 scheduler.yield(如果可用),但如果不可用,则根本不会让出:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield 是调度程序 API 中令人兴奋的新功能,希望能比当前的让出策略更方便开发者提高响应速度。如果您觉得 scheduler.yield 是一个有用的 API,请参与我们的调研以帮助我们改进,并提供反馈以告诉我们如何进一步改进。

主打图片来自 Unsplash,作者:Jonathan Allison