使用 scheduler.yield() 拆分长任务

Brendan Kenny
Brendan Kenny

发布时间:2025 年 3 月 6 日

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: 142.
  • Safari: not supported.

Source

长时间运行的任务使主线程一直处于繁忙状态,导致其无法执行其他重要工作(例如响应用户输入)时,网页会显得迟缓且无响应。因此,即使是内置的表单控件,用户也可能会觉得它们出了问题(好像页面冻结了),更不用说更复杂的自定义组件了。

scheduler.yield() 是一种向主线程让步的方式,可让浏览器运行任何待处理的高优先级工作,然后从中断处继续执行。这样可使网页更具响应性,进而有助于改善 Interaction to Next Paint (INP)

scheduler.yield 提供了一个符合人体工程学的 API,可实现其名称所指的功能:在 await scheduler.yield() 表达式处暂停调用它的函数的执行,并让位于主线程,从而分解任务。该函数的其余部分(称为函数的延续)的执行将安排在新的事件循环任务中运行。

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

scheduler.yield 的具体好处在于,在 yield 之后的延续部分会被安排在运行网页已排入队列的任何其他类似任务之前运行。它会优先考虑继续执行任务,而不是开始执行新任务。

setTimeoutscheduler.postTask 等函数也可用于分解任务,但这些延续通常在所有已排队的新任务之后运行,可能会导致从让出主线程到完成工作之间出现长时间的延迟。

在让出后优先执行延续

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

优先级任务调度只是让这一点更加明确,从而更轻松地确定哪个任务将在另一个任务之前运行,并能够根据需要调整优先级来更改执行顺序。

如上所述,在通过 scheduler.yield() 生成后继续执行函数的优先级高于启动其他任务。指导性概念是,任务的延续应先运行,然后再继续执行其他任务。如果任务是行为良好的代码,会定期让步,以便浏览器可以执行其他重要操作(例如响应用户输入),那么它不应因让步而受到惩罚,在其他类似任务之后获得优先处理。

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

setTimeout(myJob);
setTimeout(someoneElsesJob);

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

在开发者工具中,该工作可能如下所示:

Chrome 开发者工具的“性能”面板中显示了两项任务。两者都表示为长时间运行的任务,其中函数“myJob”占据了第一个任务的整个执行时间,“someoneElsesJob”占据了第二个任务的整个执行时间。

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

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

由于 myJobPart2 计划在 myJob 内与 setTimeout 一起运行,但该调度是在 someoneElsesJob 已被调度之后运行的,因此执行情况如下:

Chrome 开发者工具的“性能”面板中显示了三个任务。第一个是运行函数“myJobPart1”,第二个是运行“someoneElsesJob”的长任务,最后一个任务是运行“myJobPart2”。

我们使用 setTimeout 将任务拆分,以便浏览器在 myJob 的中间阶段保持响应,但现在 myJob 的第二部分仅在 someoneElsesJob 完成后运行。

在某些情况下,这可能没问题,但通常并非最佳做法。myJob 会让位于主线程,以确保网页能够对用户输入保持响应,但不会完全放弃主线程。如果 someoneElsesJob 特别慢,或者除了 someoneElsesJob 之外还安排了许多其他作业,那么可能需要很长时间才能运行 myJob 的后半部分。这可能不是开发者在向 myJob 添加 setTimeout 时所希望的。

输入 scheduler.yield(),这会将调用它的任何函数的延续置于比启动任何其他类似任务略高的优先级队列中。如果 myJob 更改为使用它:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

现在,执行情况如下所示:

Chrome 开发者工具的“性能”面板中显示了两项任务。两者都表示为长时间运行的任务,其中函数“myJob”占据了第一个任务的整个执行时间,“someoneElsesJob”占据了第二个任务的整个执行时间。

浏览器仍有机会做出响应,但现在 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 的小型填充区,可在内部使用多种方法的组合来模拟其他浏览器中调度 API 的许多功能(但不支持 scheduler.yield() 优先级继承)。

对于希望避免使用 Polyfill 的用户,一种方法是使用 setTimeout() 进行让步,并接受优先级较高的延续丢失,或者在不支持的浏览器中不进行让步(如果无法接受)。如需了解详情,请参阅“优化长时间运行的任务”中的 scheduler.yield() 文档

如果您要自行检测 scheduler.yield() 功能并添加回退,还可以使用 wicg-task-scheduling 类型来获取类型检查和 IDE 支持。

了解详情

如需详细了解该 API 及其与任务优先级和 scheduler.postTask() 的互动方式,请参阅 MDN 上的 scheduler.yield()优先安排任务文档。

如需详细了解长任务、它们对用户体验的影响以及如何处理它们,请参阅有关优化长任务的文章。