w3ctech

使用 scheduler.yield() 让出浏览器主线程控制权,来分解长任务

本文英文原文:https://developer.chrome.com/blog/use-scheduler-yield

当长任务长时间占用主线程时,页面会变得迟钝且响应缓慢,因为它阻止了主线程执行其他重要工作,例如响应用户输入。结果是,即便是内置的表单控件,在用户看来也可能像是出了问题——仿佛页面卡住了一样——更不用说那些更复杂的自定义组件了。

scheduler.yield() 是一种让出主线程控制权的方法——允许浏览器运行任何待处理的高优先级工作——然后在之前中断的地方继续执行。这使得页面响应更灵敏,进而有助于改善“下次绘制交互 (INP)”指标。

scheduler.yield 提供了一个符合其名称的人性化 API:它会在 await scheduler.yield() 表达式处暂停当前函数的执行,并将控制权让给主线程,从而分解任务。函数的其余部分(称为函数的“延续”)将被安排在一个新的事件循环任务中运行。

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // 让出主线程。
  slowerComputation();
}

scheduler.yield 的具体好处在于,让出控制权之后的延续部分,会被安排在页面已排队的任何其他类似任务之前运行。它将任务的延续优先级置于启动新任务之上。

setTimeoutscheduler.postTask 这样的函数也可以用来分解任务,但这些函数的延续通常会在任何已经排队的新任务之后运行,这可能会导致在让出主线程和完成其工作之间存在较长的延迟。

让出后的优先延续

scheduler.yield 是“优先级任务调度 API”的一部分。作为 Web 开发人员,我们通常不会根据明确的优先级来讨论事件循环运行任务的顺序,但相对优先级始终存在,例如 requestIdleCallback 回调会在任何已排队的 setTimeout 回调之后运行,或者触发的输入事件监听器通常会在使用 setTimeout(callback, 0) 排队的任务之前运行。

优先级任务调度只是让这一点更明确,更容易弄清楚哪个任务将在另一个任务之前运行,并且允许在需要时调整优先级以更改执行顺序。

如前所述,使用 scheduler.yield() 让出控制权后,函数的继续执行会获得比启动其他任务更高的优先级。其指导思想是,任务的延续应该首先运行,然后再处理其他任务。如果任务是行为良好的代码,它会定期让出控制权,以便浏览器可以执行其他重要操作(如响应用户输入),那么它不应该因为让出控制权而受到惩罚,被排在其他类似任务之后。

以下是一个示例:两个函数,使用 setTimeout 排队在不同的任务中运行。

setTimeout(myJob);
setTimeout(someoneElsesJob);

在这种情况下,两个 setTimeout 调用紧挨在一起,但在实际页面中,它们可能在完全不同的地方被调用,例如第一方脚本和第三方脚本独立设置要运行的工作,或者它可能是来自不同组件的两个任务在您的框架调度程序深处被触发。

以下是这些工作在 DevTools 中的样子:

DevTools

myJob 被标记为一个长任务,在它运行时阻止浏览器执行任何其他操作。假设它来自第一方脚本,我们可以将其分解:

function myJob() {
  // 运行第 1 部分。
  myJobPart1();
  // 使用 setTimeout() 让出控制权以分解长任务,然后运行 part2。
  setTimeout(myJobPart2, 0);
}

因为 myJobPart2 是在 myJob 内部使用 setTimeout 安排运行的,但在 someoneElsesJob 已经被安排之后才进行调度,所以执行情况如下:

DevTools

我们使用 setTimeout 分解了任务,以便浏览器可以在 myJob 执行期间保持响应,但现在 myJob 的第二部分仅在 someoneElsesJob 完成后运行。

在某些情况下,这可能没问题,但通常这并不是最佳选择。myJob 让出主线程是为了确保页面可以对用户输入保持响应,而不是完全放弃主线程。如果 someoneElsesJob 特别慢,或者除了 someoneElsesJob 之外还安排了许多其他作业,那么可能需要很长时间才能运行 myJob 的后半部分。当开发者在 myJob 中添加 setTimeout 时,这可能不是他们的本意。

现在引入 scheduler.yield(),它将调用它的任何函数的延续放在一个比启动任何其他类似任务优先级稍高的队列中。如果将 myJob 更改为使用它:

async function myJob() {
  // 运行第 1 部分。
  myJobPart1();
  // 使用 scheduler.yield() 让出控制权以分解长任务,然后运行 part2。
  await scheduler.yield();
  myJobPart2();
}

现在执行情况如下:

DevTools

浏览器仍然有机会保持响应,但现在 myJob 任务的延续优先于启动新任务 someoneElsesJob,因此 myJobsomeoneElsesJob 开始之前完成。这更接近于让出主线程以保持响应性的期望,而不是完全放弃主线程。

优先级继承

作为更大的优先级任务调度 API 的一部分,scheduler.yield() 可以很好地与 scheduler.postTask() 中可用的显式优先级组合。如果没有显式设置优先级,scheduler.postTask() 回调中的 scheduler.yield() 的行为基本上与前面的示例相同。

但是,如果设置了优先级,例如使用较低的“background”优先级:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

延续将以高于其他“background”任务的优先级进行调度——在任何待处理的“background”工作之前获得预期的优先延续——但仍然低于其他默认或高优先级任务;它仍然是“background”工作。

这意味着,如果您使用“background” scheduler.postTask()(或使用 requestIdleCallback)安排低优先级工作,则其中 scheduler.yield() 之后的延续也将等到大多数其他任务完成并且主线程空闲时才运行,这正是您希望从低优先级作业中让出控制权所获得的结果。

如何使用 API

目前,scheduler.yield() 仅在基于 Chromium 的浏览器中可用,因此要使用它,您需要进行特性检测,并在其他浏览器中回退到辅助的让出控制权方式。

scheduler-polyfillscheduler.postTaskscheduler.yield 的一个小型 polyfill,它在内部使用多种方法的组合来模拟其他浏览器中调度 API 的大部分功能(尽管不支持 scheduler.yield() 优先级继承)。

对于那些希望避免使用 polyfill 的人来说,一种方法是使用 setTimeout() 让出控制权并接受失去优先延续,或者如果这是不可接受的,甚至在不受支持的浏览器中不让出控制权。有关更多信息,请参阅“优化长任务”中的 scheduler.yield() 文档

如果您正在进行 scheduler.yield() 的特性检测并自己添加回退方案,则还可以使用 wicg-task-scheduling 类型来获得类型检查和 IDE 支持。

了解更多

有关 API 及其如何与任务优先级和 scheduler.postTask() 交互的更多信息,请查看 MDN 上的 scheduler.yield() 和优先级任务调度文档。

要了解有关长任务、它们如何影响用户体验以及如何处理它们的更多信息,请阅读有关优化长任务的内容。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复