大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

  Google工程师:为何 scheduler.yield() 号称 2025 前端性能必杀技?1、为什么需要 scheduler.yield()

  Scheduler.yield() 方法用于在任务执行期间将线程控制权交还给主线程,并在稍后继续执行,且后续执行会被调度为优先级更高的任务。这使得长时间运行的任务可以被拆分,从而保持浏览器的响应速度。

  当 yield() 方法返回的 Promise 被 resolve 后,任务即可继续执行。Promise resolve 后的优先级默认为 user-visible,但如果 yield() 调用发生在 Scheduler.postTask() 回调函数中则可以继承不同的优先级,这一点在后文会详述。

  同时,如果 yield() 调用之后的工作发生在 postTask() 回调中,且该任务被中止,则后续工作可以被取消。

  下面示例用于演示:当在主线程上需要执行耗时较长的工作,而这些工作可以分成一系列任务的情况下,可以反复调用 scheduler.yield() 来保持页面始终具有响应性。

  function doWork(value) { console.log(`work chunk ${value}`);}const workList = [0, 1, 2, 3, 4];for (const work of workList) { doWork(work); await scheduler.yield();}2、scheduler.yield() 继续执行的优先级高于 scheduler.postTask()

  scheduler.yield() 返回的 Promise 相对于其他任务的解析顺序取决于一个隐式的任务优先级。

  默认情况下,scheduler.yield() 以 user-visible 优先级运行。但是,当在调用 scheduler.yield() 之后执行的后续任务与相同优先级的 scheduler.postTask() 行为略有不同。

  即与相同优先级的 scheduler.postTask() 相比,scheduler.yield() 会将任务放入一个优先级更高的任务队列中。因此一个优先级为 user-visible 的 scheduler.yield() 后续任务的优先级会排在优先级更高的 user-blocking 级别的 scheduler.postTask() 任务之后,但在相同优先级的 scheduler.postTask() 任务之前。

  总之,scheduler.yield() 将任务排在优先级队列的头部,scheduler.postTask() 则排在队列的末尾。在任务较少且优先级相同的情况下,scheduler.yield() 执行的任务会优先执行,从而为任务调度提供了更大的灵活性。

  // 输出顺序为 user-blocking postTask 、user-visible yield 、user-visible postTaskscheduler.postTask(() => console.log("user-visible postTask"));scheduler.postTask(() => console.log("user-blocking postTask"), { priority: "user-blocking",});await scheduler.yield();console.log("user-visible yield");

  在多次调用 scheduler.yield() 时,scheduler.yield() 的后续任务会被放入优先级更高的队列特性就显得尤为重要,即第二个 scheduler.yield() 任务不会在队列中已有的任务之前执行。

  即如果一个函数在第二个函数之前让出主线程,则先让出的函数会优先继续执行,例如:

  async function first() { console.log("starting first function"); await scheduler.yield(); console.log("ending first function");}async function second() { console.log("starting second function"); await scheduler.yield(); console.log("ending second function");}first();second();

  此时控制台输出结果为:

  starting first functionstarting second functionending first functionending second function3、scheduler.postTask() 中的 scheduler.yield() 会继承当前任务优先级

  需要注意的是,在 scheduler.postTask() 中调用 scheduler.yield() 将继承前者的任务优先级。例如,在低优先级 background 任务中调用 scheduler.yield() 之后执行的工作默认会被调度为 background 优先级。

  但同样,其会被插入到优先级更高的 background 队列中,因此会在任何 background postTask() 任务之前运行。

  async function backgroundWork() { scheduler.postTask(() => console.log("background postTask"), { priority: "background", }); scheduler.postTask(() => console.log("user-visible postTask"), { priority: `user-visible`, }); // scheduler.yield() 从外部的任务继承 `background` 优先级 await scheduler.yield(); console.log("default-background yield");}await scheduler.postTask(backgroundWork, { priority: "background"});

  此时控制台将会输入如下内容:

  user-visible postTaskdefault-background yieldbackground postTask

  如果开发者需要动态更改任务优先级,开发者不要显式设置 options.priority 参数,而应创建一个 TaskController,并将其 TaskSignal 传递给 options.signal。此时任务优先级将根据信号优先级初始化,之后可以使用与该信号关联的 TaskController 进行修改。

  // 将 TaskController 的初始信号优先级设置为'user-blocking'const controller = new TaskController({priority: "user-blocking"});// 监听 controller 的信号的 prioritychange 事件controller.signal.addEventListener("prioritychange", (event) => { const previousPriority = event.previousPriority; const newPriority = event.target.priority; console.log(`Priority changed from ${previousPriority} to ${newPriority}.`);});// 使用 controller.signal 创建一个任务scheduler.postTask(() => console.log("Task 1"), { signal: controller.signal });// 使用 controller 将任务优先级设置为'background'controller.setPriority("background");

  此时控制台输出如下:

  Priority changed from user-blocking to background.Task 1

  需要注意的是,上面示例中优先级是在任务执行之前更改的,但也可以在任务运行时动态更改。

3、通过 TaskController 终止 scheduler.postTask() 来间接取消 scheduler.yield() 调用

  与设置优先级类似,scheduler.yield() 调用不能直接中止,但其会继承来自其外层 scheduler.postTask() 任务的中止信号,中止该任务也会中止其中所有待处理的 scheduler.yield() 调用。

  下面示例使用 TaskController 来中止包含 scheduler.yield() 的任务。

  const taskController = new TaskController();function firstHalfOfWork() { console.log("first half of work"); taskController.abort("cancel work");}function secondHalfOfWork() { // 下面代码永远不会执行 console.log("second half of work");}scheduler.postTask( async () => { firstHalfOfWork(); await scheduler.yield(); secondHalfOfWork(); }, {signal: taskController.signal});

  在上面的例子中,abort() 发生在 scheduler.postTask() 任务启动之后,但 scheduler.yield() 调用继承了中止信号,因此 await scheduler.yield() 调用将抛出中止错误,原因为 cancel work。

4、如何通过 scheduler.postTask() 延迟任务执行

  开发者可以使用 postTask() 函数的 options.delay 参数中指定一个以毫秒为单位的整数值来延迟任务。此时会将任务添加到优先级队列中,并在指定 delay 后执行,与 setTimeout() 类似。delay 时间是任务添加到调度器之前所需的最短时间,实际执行可能更长。

  以下代码展示了如何添加两个任务,并设置了延迟时间。

  scheduler .postTask(() => "Task delayed by 2000ms", { delay: 2000 }) .then((taskResult) => console.log(`${taskResult}`));scheduler .postTask(() => "Next task should complete in about 2000ms", { delay: 1 }) .then((taskResult) => console.log(`${taskResult}`));5、在 requestIdleCallback() 中使用 yield()

  当从回调函数内部调用 scheduler.yield() 时,scheduler.yield() 调用也会继承 Window.requestIdleCallback() 的优先级,即继承 background 优先级值。

  但需要注意的是:在 requestIdleCallback() 回调函数内部调用 scheduler.yield() 是 不可中止 的。

参考资料