构建能够快速响应用户输入的网站一直是 Web 性能中最具挑战性的方面之一,Chrome 团队一直在努力帮助 Web 开发者应对这一挑战。就在今年,我们宣布,Interaction to Next Paint (INP) 指标将从实验性指标升级为待定指标。它现在已准备好在 2024 年 3 月取代 First Input Delay (FID) 成为一项核心 Web 指标。
为了不断提供新的 API 来帮助 Web 开发者尽可能提高网站的响应速度,Chrome 团队目前正在 Chrome 115 版中运行 源试用 scheduler.yield。scheduler.yield 是对调度器 API 的一项拟议新增功能,与 传统上依赖的方法相比,它提供了一种更简单、更好的方式将控制权返回到主线程。
关于让出
JavaScript 使用运行到完成模型来处理任务。这意味着,当任务在主线程上运行时,该任务会根据需要运行以完成。任务完成后,控制权会返回给主线程,这允许主线程处理队列中的下一个任务。
除了任务永远不会完成的极端情况(例如无限循环)之外,让出是 JavaScript 任务调度逻辑中不可避免的一个方面。它一定会发生,只是时间问题,而且越早越好。如果任务运行时间过长(确切地说,超过 50 毫秒),则会被视为长任务。
长任务是导致网页响应速度低下的原因,因为它们会延迟浏览器响应用户输入的能力。长任务发生的频率越高,运行时间越长,用户就越有可能认为网页运行缓慢,甚至感觉网页完全损坏。
不过,仅仅因为您的代码在浏览器中启动了任务,并不意味着您必须等到该任务完成,控制权才会返回给主线程。您可以通过在任务中显式让出,来提高网页对用户输入的响应速度,这会将任务分解为多个部分,以便在下一个可用机会完成。这样,其他任务就可以比等待长任务完成更快地在主线程上获得时间。
当您显式让出时,您是在告诉浏览器:“嘿,我知道我即将要做的工作可能需要一段时间,我不希望您必须完成所有这些工作,然后才能响应用户输入或其他可能也很重要的任务”。 这是开发者工具箱中的一个宝贵工具,可以大大改善用户体验。
当前让出策略存在的问题
的部分。一种常见的让出方法是使用 ,超时值为 setTimeout0。之所以有效,是因为传递给 setTimeout 的回调会将剩余的工作移到单独的任务中,该任务将排队等待后续执行。您不是等待浏览器自行让出,而是说“将这个大块工作分解成更小的部分”。
不过,使用 setTimeout 让出可能会产生不希望有的副作用:让出点之后的工作将移到任务队列的末尾。 由用户互动调度的任务仍会按预期移到队列的前面,但您希望在显式让出后完成的剩余工作可能会因排在前面的其他来源的任务而进一步延迟。
如需查看实际效果,请试用此 Codepen 演示,或在以下嵌入式版本中进行实验。该演示包含几个您可以点击的按钮,以及它们下方的一个框,用于记录任务的运行时间。当您进入该页面时,请执行以下操作:
- 点击顶部标记为定期运行任务 的按钮,该按钮将调度阻塞任务以定期运行。点击此按钮后,任务日志将填充多条消息,内容为使用
setInterval运行阻塞任务。 - 接下来,点击标记为运行循环,在每次迭代时使用
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 让出。
这说明了 Web 上一个常见的问题:脚本(尤其是第三方脚本)注册一个计时器函数以按一定间隔运行工作并不罕见。使用 setTimeout 让出时出现的“任务队列结束”行为意味着,来自其他任务来源的工作可能会排在循环在让出后必须完成的剩余工作之前。
根据您的应用,这可能是理想的结果,也可能不是,但在许多情况下,这种行为是开发者可能不愿意轻易放弃对主线程的控制权的原因。让出是好的,因为用户互动有机会更快地运行,但它也允许其他非用户互动工作在主线程上获得时间。这是一个真正的问题,但 scheduler.yield 可以帮助解决这个问题!
输入 scheduler.yield
scheduler.yield 自 Chrome 115 版以来,一直作为实验性 Web 平台功能在标志后提供。您可能会问:“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。 - 启用实验性 Web 平台功能 实验。您可能需要在执行此操作后重启 Chrome。
- 前往演示页面,或使用此列表后面的以下嵌入式版本。
- 点击顶部标记为定期运行任务 的按钮。
- 最后,点击标记为运行循环,在每次迭代时使用
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 版开始通过两种方式进行试用:
- 如果您想在本地试用
scheduler.yield,请在 Chrome 的地址栏中输入并输入chrome://flags,然后从实验性 Web 平台功能 部分的下拉菜单中选择启用 。这样,scheduler.yield(以及任何其他实验性功能)将仅在您的 Chrome 实例中可用。 - 如果您想在公开可访问的来源上为真正的 Chromium 用户启用
scheduler.yield,则需要注册scheduler.yield源试用。这样,您就可以在给定的时间段内安全地试用拟议的功能,并让 Chrome 团队深入了解这些功能在实际应用中的使用情况。如需详细了解源试用的运作方式,请参阅本指南。
您如何使用 scheduler.yield(同时仍支持未实现它的浏览器)取决于您的目标。您可以使用官方 Polyfill。如果以下情况适用于您,则 Polyfill 非常有用:
- 您已在应用中使用
scheduler.postTask来调度任务。 - 您希望能够设置任务和让出优先级。
- 您希望能够使用
scheduler.postTaskAPI 提供的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,请参与我们的研究以帮助改进它,并 提供反馈 有关如何进一步改进它的信息。
主打图片来自 Unsplash,由 Jonathan Allison 提供。