Back

使用 Scheduler API 在浏览器中处理后台任务

使用 Scheduler API 在浏览器中处理后台任务

你的应用感觉很快,直到它不再快为止。用户点击一个按钮,结果 300 毫秒内毫无反应,因为主线程正忙于处理一些本不需要立即执行的事情。这正是 Prioritized Task Scheduling API(通常称为 Scheduler API)旨在解决的核心问题。

本文介绍 scheduler.postTask()scheduler.yield() 如何为你提供对主线程调度的实用控制,何时该用它们替代旧方法,以及当前的浏览器支持情况。

核心要点

  • Scheduler API 为你提供对主线程工作执行时机和方式的精细控制,支持三种优先级:user-blockinguser-visiblebackground
  • scheduler.postTask() 以显式优先级延迟执行工作,而 scheduler.yield() 则将长任务拆分,让浏览器能够在步骤之间处理用户输入。
  • 这两种方法都不会将工作移出主线程。若需要真正的并行执行,请使用 Web Workers。
  • Chromium 系浏览器和 Firefox 支持该 API;Safari 尚不支持,因此始终需要包含 setTimeout 回退方案。
  • 更智能的调度可以改善 Interaction to Next Paint(INP)指标和整体响应性。

为什么主线程调度很重要

JavaScript 运行在单线程上。每一次脚本执行、DOM 更新和事件处理都在争用同一资源。当一个长任务阻塞了该线程时,浏览器就无法响应用户输入——这会直接损害你的 Interaction to Next Paint (INP) 分数。

传统的变通方法各有局限:

  • setTimeout(fn, 0) 能延迟工作,但无法控制优先级。无论线程多忙它都会执行。
  • requestIdleCallback() 等待空闲时间,适合低优先级工作,但没有优先级体系,Safari 支持有限,并且在负载下可能被无限期推迟。

Scheduler API 同时解决了这两个问题。

scheduler.postTask() 的工作原理

scheduler.postTask() 是 Prioritized Task Scheduling API 的主要入口。它以指定的优先级在主线程上调度一个回调执行。

scheduler.postTask(() => {
  sendAnalyticsEvent('page_view');
}, { priority: 'background' });

共有三个优先级:

  • user-blocking — 最高优先级,用于直接阻塞用户交互的工作
  • user-visible — 默认值,用于影响用户可见内容但非阻塞的工作
  • background — 最低优先级,用于埋点、预取或清理

这正是它与旧 API 的关键区别:你不只是在延迟工作,而是在告诉浏览器这项工作相对于队列中的其他工作有多重要

scheduler.postTask() 返回一个 Promise,方便配合 async/await 使用并干净地处理错误:

try {
  await scheduler.postTask(() => processLargeDataset(data), {
    priority: 'background'
  });
} catch (err) {
  // 任务被中止或失败
  console.warn('Task did not complete:', err);
}

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

scheduler.yield() 是较新加入的方法,用于解决一个不同的问题:当你已经身处一个长任务中,需要在继续之前给浏览器机会处理输入时该怎么办?

async function processItems(items) {
  for (const item of items) {
    processItem(item);
    await scheduler.yield(); // 在每项之间将控制权交还浏览器
  }
}

每一次 await scheduler.yield() 都会创建一个检查点,让浏览器能在恢复你的循环之前处理待响应的用户交互。默认情况下,yield() 之后的延续以 user-visible 优先级调度,但它也可以从外层的 postTask() 调用继承优先级。这是减少长任务、又无需重构整个代码库的最实用工具之一。

一个重要的澄清

无论是 scheduler.postTask() 还是 scheduler.yield(),都不会将工作移出主线程。这样调度的任务仍在主线程上运行——只不过它们的入队和优先级安排更智能。如果你需要真正的并行执行,那就需要使用 Web Workers

浏览器支持

Chromium 系浏览器(Chrome、Edge、Opera)对 Scheduler API 的支持已经相当稳固。Firefox 的支持比 Chromium 晚得多,Safari 目前仍不支持该 API。你可以在 Can I Use 上查询当前的支持矩阵。

使用前的最小特性检测如下:

if ('scheduler' in window && 'postTask' in scheduler) {
  scheduler.postTask(myTask, { priority: 'background' });
} else {
  // Safari 和旧浏览器的回退方案
  setTimeout(myTask, 0);
}

特别是对于 scheduler.yield(),在生产环境中依赖它之前应单独检测支持,因为它比 postTask() 上线更晚,覆盖范围更窄:

async function safeYield() {
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  } else {
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

何时使用 Scheduler API

当你需要延迟非关键性工作——埋点、预取或渲染后处理——并希望显式控制优先级时,使用 scheduler.postTask()。当你的循环或多步流程有阻塞输入处理风险时,使用 scheduler.yield()

如果你的用户主要使用 Chromium 或 Firefox,配合特性检测和回退方案,Scheduler API 在今天已经足够实用。对于更广泛的覆盖,请保留 setTimeout 回退,直到 Safari 跟上。

结语

Scheduler API 填补了 Web 平台在主线程工作处理方面长期存在的空白。你不再需要依赖 setTimeout(fn, 0) 这样的粗糙工具,或支持参差不齐的 requestIdleCallback(),而是可以表达意图:这个任务是关键的,那个可以稍后处理,这个循环应在用户输入时暂停。结果可能是更流畅的交互和更好的 INP 分数,而代码改动相对很小。配上特性检测和合理的回退,你今天就可以安全地采用它。

常见问题

不会。所有通过 scheduler.postTask() 调度的任务仍在主线程上执行。该 API 仅通过分配优先级来改变任务运行的时机和顺序。如果你需要为 CPU 密集型工作实现真正的并行,请改用 Web Workers,它在独立线程上运行代码,并通过消息与主线程通信。

当你想在现有函数或循环中短暂暂停以便浏览器处理用户输入然后再继续时,使用 scheduler.yield()。当你想以特定优先级在稍后调度一个离散的工作单元时,使用 scheduler.postTask()。它们解决相关但不同的问题:任务执行中让出 vs 排队新任务。

INP 衡量页面对用户交互的响应速度。主线程上的长任务是 INP 表现不佳的最常见原因。通过在步骤之间让出控制权,并为非关键工作分配较低优先级,Scheduler API 有助于保持主线程可用,及时处理点击、轻触和按键,从而降低交互延迟。

是的,只要你包含特性检测和回退方案即可。一个简单的模式是检测 scheduler.postTask,不可用时回退到 setTimeout。这样能让你的代码在 Safari 和旧浏览器中正常工作,同时让 Chromium 和 Firefox 用户享受优先级调度带来的好处。Polyfill 是存在的,但对基本用法通常不必要。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay