构建能够快速响应用户输入的网站一直是网络性能最具挑战性的方面之一,Chrome 团队一直致力于帮助网络开发者结识这一难题。就在今年,我们宣布了Interaction to Next Paint (INP) 指标将从实验阶段转为待处理状态。现在,我们将于 2024 年 3 月取代 First Input Delay (FID),成为 Core Web Vitals 指标。
为了持续提供新的 API 来帮助 Web 开发者打造尽可能简洁的网站,Chrome 团队目前正在从 Chrome 115 版开始运行针对 scheduler.yield
的源试用。scheduler.yield
是调度器 API 中提议的新增功能,与传统上依赖的方法相比,它提供了一种更简单、更出色的方式,将控制权交还给主线程。
收益时
JavaScript 使用运行到完成模式来处理任务。这意味着,当某个任务在主线程上运行时,该任务会在必要时运行,以便完成。任务完成后,控制权将交还给主线程,以便主线程处理队列中的下一个任务。
除了任务永远无法完成的极端情况(例如无限循环)之外,收益是 JavaScript 任务调度逻辑中不可避免的方面。它会发生,只是时间问题,而且越早越好。如果任务的运行时间过长(确切地说超过 50 毫秒),则会被视为“耗时较长的任务”。
耗时较长的任务会导致网页响应速度缓慢,因为这些任务会延迟浏览器响应用户输入的能力。耗时较长的任务发生得越频繁,运行时间越长,用户就越有可能认为网页运行缓慢,甚至觉得网页完全已损坏。
但是,仅仅因为您的代码在浏览器中启动任务,并不意味着您必须等待该任务完成,然后控制权才会交还给主线程。您可以通过在任务中显式让出执行,将任务分解成在下一个可用机会时完成,从而提高对页面上用户输入的响应的响应速度。这样,与必须等待耗时较长的任务完成的任务相比,这些任务可以更快地在主线程上处理时间。
<ph type="x-smartling-placeholder">当您明确让行执行时,就等于告诉浏览器:“我知道我要执行的工作可能需要一些时间,而且我不希望您在响应用户输入或其他可能很重要的任务之前必须完成所有这些工作”。它是开发者工具箱中的一种宝贵工具,对改善用户体验大有帮助。
当前收益策略存在的问题
常见的产生方法会使用超时值为 0
的 setTimeout
。此方法之所以行之有效,是因为传递给 setTimeout
的回调会将剩余工作移至单独的任务中,而该任务将排队等待后续执行。您不是等待浏览器自行做出让位,而是说“让我们将这一大块的工作分解成更小的片段”。
不过,通过 setTimeout
执行挂起可能会带来不良的副作用:在挂起点之后执行的工作将回到任务队列的后端。由用户互动安排的任务仍然会按照预期方式进入队列前端,但是您在显式让出后想做的其余工作最终可能会被其他竞争来源中的先于队列的任务所延迟。
如需查看实际效果,请尝试此 Glitch 演示,或在下面的嵌入式版本中进行实验。该演示包含一些您可以点击的按钮,以及这些按钮下方的框(用于记录任务的运行时间)。进入该页面后,请执行以下操作:
- 点击标有定期运行任务的顶部按钮,这将安排阻止任务每隔一段时间运行一次。点击此按钮后,任务日志会显示几条消息,内容为通过
setInterval
运行阻止任务。 - 接下来,点击标签为 Run 您的商家循环,每次迭代时通过
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
。
这说明了网络上的一个常见问题:脚本(尤其是第三方脚本)注册按一定间隔运行工作的计时器函数的情况很常见。“任务队列结尾”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
的实际效果,请执行以下操作:
- 导航到
chrome://flags
。 - 启用实验性网络平台功能实验。执行此操作后,您可能需要重新启动 Chrome。
- 前往演示页面或使用此列表下方的嵌入式版本。
- 点击标有定期运行任务的顶部按钮。
- 最后,点击标签为 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
生成的演示不同,您可以看到,循环(即使每次迭代后都会生成)不会将剩余工作发送到队列后部,而是发送到队列前面。这样既可以两全其美:您可以通过利用收益来提升网站上的输入响应速度,但还能确保您希望在生成后完成的工作不会延误。
快来试试吧!
如果您对 scheduler.yield
很感兴趣,并想试用该功能,可以从 Chrome 115 版开始,通过以下两种方式实现:
- 如果您想在本地对
scheduler.yield
进行实验,请在 Chrome 地址栏中输入并输入chrome://flags
,然后从“实验性 Web 平台功能”部分的下拉菜单中选择启用。这样一来,scheduler.yield
(以及任何其他实验性功能)将只能在您的 Chrome 实例中使用。 - 如果您想在可公开访问的来源中为真实的 Chromium 用户启用
scheduler.yield
,则需要注册参与scheduler.yield
源试用。这样一来,您便能够放心地对提议的功能进行给定的一段实验时间,并让 Chrome 团队深入了解这些功能在实际应用中的使用情况。如需详细了解源试用的运作方式,请参阅此指南。
您应如何使用 scheduler.yield
(同时仍能支持未实现它的浏览器)取决于您的目标。您可以使用官方 polyfill。如果您的情况符合以下情况,则 polyfill 会非常有用:
- 您已经在应用中使用
scheduler.postTask
来安排任务。 - 您需要能够设定任务并确定优先次序。
- 您希望能够通过
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,请参与我们的调研以帮助我们改进,并提供反馈以告诉我们如何进一步改进。
主打图片来自 Unshot,由 Jonathan Allison 制作。