推出 scheduler.yield 源试用

构建能够快速响应用户输入的网站一直是网络性能中最具挑战性的方面之一,Chrome 团队一直在努力帮助 Web 开发者满足这一要求。就在今年,我们宣布Interaction to Next Paint (INP) 指标将从实验状态降级为待处理状态。它现已准备好在 2024 年 3 月取代 First Input Delay (FID),成为 Core Web Vitals 指标。

为了一如既往地提供新的 API 来协助 Web 开发者打造尽可能简洁的网站,Chrome 团队目前正在进行针对 scheduler.yield 的源试用(从 Chrome 115 版开始)。scheduler.yield 是调度器 API 的新增一项功能,与传统所依赖的方法相比,通过更简单的方法,将控制权交还给主线程。

退让时

JavaScript 使用“运行到完成”模型来处理任务。也就是说,当一个任务在主线程上运行时,该任务会按照为完成而必要的时间运行。任务完成后,控制权将交还给主线程,这让主线程能够处理队列中的下一个任务。

除了任务永不完成的极端情况(如无限循环)之外,退让是 JavaScript 任务调度逻辑中不可避免的一个方面。事情一定会发生,只是时间问题,越早越好。如果任务的运行时间过长(确切地说超过 50 毫秒),则会被视为耗时较长的任务。

耗时较长的任务会导致网页响应速度变慢,因为它们会延迟浏览器响应用户输入的能力。耗时较长的任务出现的频率越高,运行时间越长,用户就越有可能认为网页运行缓慢,甚至觉得网页完全损坏。

不过,代码在浏览器中启动某个任务,并不意味着您必须等到该任务完成后才能将控制权交还给主线程。通过在任务中显式让让,可以提高对页面上用户输入的响应速度,这会将任务分解为在下一个可用的机会完成。与必须等待较长的任务完成相比,这让其他任务可以在主线程上更快地获得时间。

描述拆分任务如何有助于提升输入响应速度。在顶部,较长的任务会阻止事件处理脚本运行,直到任务完成为止。在底部,分块任务可让事件处理程序比其他运行时更早运行。
直观呈现将控制权交还给主线程。总的来说,只有在任务运行完成之后才会让退让,这意味着任务可能需要更长时间才能完成,才能将控制权交还给主线程。在底部,让出是显式完成的,将较长的任务分解为几个较小的任务。这样可以更快地用户互动,从而提高输入响应速度和 INP。

当您明确让出让让时,就等于告诉浏览器:“嘿,我知道我要完成的工作可能需要一段时间,我不希望您在响应用户输入或其他可能重要的其他任务之前完成所有工作”。它是开发者工具箱中的宝贵工具,对改善用户体验大有帮助。

当前收益策略的问题

生成常用方法使用超时值为 0setTimeout。这种方式之所以行之有效,是因为传递给 setTimeout 的回调会将剩余工作移至将要排队等待后续执行的单独任务。而不是等待浏览器自行让出,而是在说“让我们将这一大块的工作拆分成较小的片段”。

但是,使用 setTimeout 退让可能会带来不良的副作用:在挂起点之后执行的工作将转到任务队列的后部。用户互动安排的任务仍会像往常一样排在队列前面,但是您要在明确让出后要完成的剩余工作最终可能会被排在队列前面的竞争来源的其他任务进一步延迟。

如要查看实际运作方式,可试用此 Glitch 演示,或在下面的嵌入式版本中试验。该演示包含几个可点击的按钮,以及下面的方框(用于记录任务运行时间)。进入该网页后,请执行以下操作:

  1. 点击顶部标有定期运行任务的按钮,该按钮将安排每隔一段时间运行一次阻塞任务。点击此按钮后,任务日志会显示几条消息,内容为使用 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 让步时发生的“任务队列结束”行为。运行的循环会处理 5 项内容,并在处理完每项内容后返回 setTimeout

这说明了 Web 上的一个常见问题:脚本(尤其是第三方脚本)注册按一定时间间隔运行工作的计时器函数并不罕见。通过 setTimeout 挂起带来的“任务队列结束”行为意味着,来自其他任务来源的工作可能会先于循环在让出后要执行的剩余工作之前排入队列。

根据具体应用,这可能是可取的结果,也可能不是。但在很多情况下,这种行为就是开发者可能不愿轻易放弃对主线程的控制的原因。收益可以提升,因为用户互动有机会更快地运行,但也让其他非用户互动工作也能在主线程上占用时间。这确实是个问题,但 scheduler.yield 可以帮助解决这个问题!

输入 scheduler.yield

从 Chrome 115 版开始,scheduler.yield 就作为一项实验性网络平台功能包含在标记后面。您可能会想到“为什么当 setTimeout 已经包含某个特殊函数时,我需要生成它?”

值得注意的是,产出并不是 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. 启用实验性网络平台功能实验。执行此操作后,您可能需要重启 Chrome。
  3. 前往演示页面,或使用此列表下方的嵌入式版本。
  4. 点击顶部标有定期运行任务的按钮。
  5. 最后,点击标签为 Runloop,收益组,在每次迭代中生成 scheduler.yield 的按钮。

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

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 生成的演示不同,您可以看到,即使它在每次迭代后生成,该循环也不会将剩余的工作发送到队列后方,而是发送到队列前面。这样你才能两全其美:你可以让自己提高网站输入响应速度,同时确保你想要在让出去完成之后完成的工作不会延迟。

你也试试吧!

如果您对 scheduler.yield 感兴趣,并想尝试一下,则可以从 Chrome 115 版开始,通过以下两种方式执行此操作:

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

使用scheduler.yield的方法(同时仍支持未实现该功能的浏览器)取决于您的目标。您可以使用官方 polyfill。如果您符合以下情况,则 polyfill 非常有用:

  1. 您已经在应用中使用 scheduler.postTask 来调度任务。
  2. 您需要能够设定任务和产生优先级。
  3. 您希望能够通过 scheduler.postTask API 提供的 TaskController 类取消任务或调整任务优先级。

如果这不符合您的情况,那么 polyfill 可能不适合您。在这种情况下,您可以通过几种方式发布自己的后备方案。第一种方法使用 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,欢迎参与我们的调研以帮助我们改进该 API,并提供反馈了解如何进一步改进该 API。

主打图片来自 Unsplash 用户,由 Jonathan Allison 提供。