requestIdleCallback()

后台任务的协作式调度

W3C 工作草案

关于本文件的更多信息
本版本:
https://www.w3.org/TR/2025/WD-requestidlecallback-20250521/
最新发布版本:
https://www.w3.org/TR/requestidlecallback/
最新编辑草稿:
https://w3c.github.io/requestidlecallback/
历史记录:
https://www.w3.org/standards/history/requestidlecallback/
提交历史
实现报告:
https://wpt.fyi/requestidlecallback
编辑:
Scott Haseley (Google Inc.)
前任编辑:
Ross McIlroy (Google Inc.)
Ilya Grigorik (Google Inc.)
反馈:
GitHub w3c/requestidlecallback (拉取请求, 新建议题, 打开的议题)

摘要

本文档定义了一个 API,网页作者可用来协作调度后台任务,从而不会对同一事件循环中的其他高优先级任务(如输入处理、动画和帧合成)造成延迟。用户代理基于其对当前已调度任务、vsync 截止时间、用户交互等信息的了解,能够更好地判断何时可以运行后台任务而不引入用户可感知的延迟或动画/输入卡顿。因此,使用该 API 应能实现更合理的后台任务调度,使其在浏览器本应处于空闲的时机进行。

本文档状态

本节介绍本文档在发布时的状态。当前 W3C 公开的文档及本技术报告的最新版本,可在 W3C 标准与草案索引 查阅,网址为 https://www.w3.org/TR/。

Web 性能工作组为本规范维护了一套 测试套件。详情见工作组的 实现报告。有意实现本规范的厂商 加入以下邮件列表并参与讨论。

本文档由 Web 性能工作组 作为工作草案,采用 推荐规范流程 发布。

作为工作草案发布并不代表 W3C 及其成员的背书。

本文件为草案,可能随时被更新、替换或废弃。除了作为在研工作外,不应引用本文档。

本文档由遵循 W3C 专利政策的 团队制定。 W3C 维护了 公开专利披露列表 ,涵盖本工作组相关成果;该页面也包含专利披露说明。任何知悉自身拥有的专利包含 必要权利要求 的个人,必须依据 W3C 专利政策》第6节 披露相关信息。

本文件受 2023年11月3日版《W3C 流程文件》 管辖。

1. 介绍

本节为非规范性内容。

网页通常希望在浏览器的事件循环中执行一些非关键的计算任务,但这些任务可能会占用较多时间。此类后台任务的例子包括记录分析数据、长时间的数据处理操作、客户端模板渲染以及对即将可见内容的预渲染。这些任务必须与其他时效性要求高的操作(如对输入的响应和基于脚本的动画)共享事件循环,例如通过requestAnimationFrame()实现的动画。这些后台任务通常通过setTimeout()来调度回调,在该时机内执行后台任务。

这种做法的一个缺点是,脚本作者无法告知用户代理某个setTimeout()回调是否为关键任务,还是可以推迟到浏览器空闲时执行。此外,用户代理也无法向回调提供关于它可以执行多长时间而不会延迟关键操作或造成动画、输入卡顿等用户可感知延迟的信息。因此,开发者通常的做法只是用一个很小的值调用setTimeout(),并在回调中执行最少的工作量,然后再次用setTimeout()调度后续工作。这种方法效率不高,因为为用户代理的事件循环频繁添加小任务会产生额外开销,并且依赖于用户代理在这些回调间穿插时效性工作,但由于用户代理无法准确预估每个回调的耗时,因此协调的难度较大。

本文档描述的 API 允许脚本作者请求用户代理在空闲时调度回调。用户代理会为回调传递一个截止时间,指示其预计还能保持空闲状态多久。页面作者可以利用截止时间确保这些后台任务不会影响对动画和输入等时延敏感事件的响应。

下面是使用该 API 实现后台任务的示例。

<!DOCTYPE html>
<title>使用 requestIdleCallback 调度后台任务</title>
<script>
var requestId = 0;
var pointsTotal = 0;
var pointsInside = 0;

function piStep() {
  var r = 10;
  var x = Math.random() `*` r `*` 2 - r;
  var y = Math.random() `*` r `*` 2 - r;
  return (Math.pow(x, 2) + Math.pow(y, 2) < Math.pow(r, 2))
}
function refinePi(deadline) {
  while (deadline.timeRemaining() > 0) {
    if (piStep())
      pointsInside++;
    pointsTotal++;
  }
  currentEstimate = (4 `*` pointsInside / pointsTotal);
  textElement = document.getElementById("piEstimate");
  textElement.innerHTML="圆周率估算: " + currentEstimate;
  requestId = window.requestIdleCallback(refinePi);
}
function start() {
  requestId = window.requestIdleCallback(refinePi);
}
function stop() {
  if (requestId)
    window.cancelIdleCallback(requestId);
  requestId = 0;
}
</script>
<button onclick="start()">点击我开始!</button>
<button onclick="stop()">点击我停止!</button>
<div id="piEstimate">未开始</div>

2. 空闲期

本节为非规范性内容。

在某一帧的输入处理、渲染和合成完成后,用户代理的主线程通常会处于空闲状态,直到下一个帧开始、其他待执行任务达到可运行状态或收到用户输入。该规范提供了一种方式,可通过requestIdleCallback() API 在这些本应空闲的时间段调度回调执行。

通过requestIdleCallback() API 注册的回调,将在用户代理定义的空闲期内变为可运行状态。运行空闲回调时会传递一个对应当前空闲周期结束时间的截止时间。什么构成空闲期由用户代理定义,但预期这些时间段是浏览器判断为将保持空闲的期间。

空闲期的一个例子是活跃动画期间将某一帧提交到屏幕后到下一个帧开始处理之间的间隔,如1所示。活跃动画和屏幕刷新期间,这样的空闲期会频繁出现,但通常非常短(例如在 60Hz vsync 设备上小于 16 毫秒)。

Example of an inter-frame idle period.
1 帧间空闲期示例
注意

Web 开发者应注意统计空闲回调期间所有操作的整体工作量。一些操作(如解析 promise 或触发页面布局)可能会导致回调结束后调度后续任务。在这种情况下,应用应在截止前主动让出,确保这些额外操作可在下一个帧截止前被执行。

另一种空闲期的例子是当用户代理处于空闲且没有屏幕刷新时。在这种情况下,用户代理可能没有即将到来的任务可以界定空闲周期的终止。为了避免在处理用户输入等不可预测任务时造成可感知的延迟,该类空闲周期的时长应被上限限制为50毫秒。一旦空闲期结束,在用户代理继续空闲时,可以再次安排新的空闲期,如2所示,使后台工作能在较长空闲阶段持续进行。

Example of an idle period when there are no pending frame updates.
2 没有待处理帧更新时的空闲期示例

在一个空闲周期内,用户代理会以FIFO顺序执行空闲回调,直到空闲期结束或没有剩余可运行回调为止。因此,用户代理并不保证在单个空闲周期内会全部执行当前已登记的空闲回调。剩余的空闲任务将在下一个空闲周期变为可运行状态。

注意

为获得最佳性能,建议开发者移除无意义的回调(如 requestAnimationFrame、setTimeout 等),不要让这类回调持续触发并等待事件响应,而是在事件实际到来时再调度。这种模式能提升整体效率,同时让用户代理有机会安排最长达50毫秒的长空闲回调,从而高效地执行大块后台任务。

仅在当前空闲周期开始前登记的空闲任务,才有资格在该周期内运行。因此,如果空闲回调期间又注册了另一个requestIdleCallback()回调,这个后注册的回调不会在当前空闲周期被执行。这样,空闲回调可在无法在截止前完成任务时,重新登记自身以便在下一个空闲周期继续运行——即便于实现如下示例中的代码模式,而不会因为空闲周期太短导致多次重复执行:

function doWork(deadline) {
  if (deadline.timeRemaining() <= 5) {
    // 这个任务超过5ms,所以等待我们
    // 被分配到足够长的空闲周期时再执行。
    requestIdleCallback(doWork);
    return;
  }
  // 执行工作……
}

下一个空闲周期开始时,新登记的空闲回调会被追加到可运行回调队列末尾,从而确保重新登记的回调以轮询形式被执行——先前任务的回调重新登记前,其他回调也能获得运行机会。

注意

本规范的未来版本可能允许其它调度策略。例如,在同一空闲周期内调度回调、调度到存在至少X毫秒空闲时间的周期等等。目前规范仅支持调度到下一个空闲周期,届时回调可执行逻辑或再次重新登记到下一空闲周期。

当用户代理判断网页处于非用户可见状态时,可以限制空闲周期以降低设备能耗,例如只每10秒触发一次空闲周期而非持续触发。

需要注意的是,在页面负载较高时用户代理未必能分配任何空闲CPU时间。因此,用户代理不调度任何空闲期完全是合理的,结果是通过requestIdleCallback()API登记的回调可能会无限期延后。对于希望优先使用空闲期、但又希望限定最长等待时间的开发者,可以在requestIdleCallback()options参数提供timeout属性;如果在指定超时前没运行回调,则会将其入队执行。

注意

50毫秒的最大截止时间来源于相关研究 [RESPONSETIME],结果显示人类大多会认为100毫秒内响应为瞬时反馈。将空闲回调截止时间限制为50毫秒,即使用户输入刚好在空闲任务开始后发生,用户代理仍有50毫秒响应时间,不会让用户感觉到滞后。

3. 一致性

如同标注为“非规范性”的段落,本规范中的所有作者指引、图示、示例和注释均为非规范性内容。规范中的其他内容为规范性条款。

本文档中的关键词 MUSTREQUIREDSHALL、及 SHOULD 应按BCP 14 [RFC2119] [RFC8174] 中说明解释,仅在以本样式全大写出现时,按上述定义解释。

本规范中的 IDL 片段MUST被解释为符合规范的IDL片段所要求的内容。

本规范定义了单一一致性类别:

一致性用户代理
只要用户代理满足本规范所有MUSTREQUIREDSHALL级别的标准,即被视为一致性用户代理。一致性用户代理还必须是一致性实现,即第4. Window接口扩展节中的IDL片段。

4. Window 接口扩展

下方 IDL 片段中的 partial interface 用于在 Window 对象上暴露 requestIdleCallback() 操作。

WebIDLpartial interface Window {
  unsigned long requestIdleCallback(IdleRequestCallback callback, optional IdleRequestOptions options = {});
  undefined cancelIdleCallback(unsigned long handle);
};

dictionary IdleRequestOptions {
  unsigned long timeout;
};

[Exposed=Window] interface IdleDeadline {
  DOMHighResTimeStamp timeRemaining();
  readonly attribute boolean didTimeout;
};

callback IdleRequestCallback = undefined (IdleDeadline deadline);

每个 Window 都有:

4.1 requestIdleCallback() 方法

当以指定的IdleRequestCallback 及可选IdleRequestOptions 调用 requestIdleCallback(callback, options) 时,用户代理必须执行如下步骤:

  1. window为当前的Window 对象。
  2. window空闲回调标识符加一。
  3. handlewindow当前的空闲回调标识符值。
  4. callbackhandle进行关联,并推送到window空闲请求回调列表末尾。
  5. 返回handle,然后异步继续运行本算法。
    注意

    下述步骤会并行运行,并在指定timeout属性时,排队一个类似setTimeout()的定时器。从此,空闲回调与超时回调将互相竞争并彼此取消——例如若空闲回调先被调度,则取消超时,反之亦然。

  6. options中存在timeout 属性且为正值:
    1. 等待timeout毫秒。
    2. 等待本算法所有timeout加上其发起时间先于当前请求的实例均已完成。
    3. 可选:再等待一段用户代理自定义的时长。
      注意

      此举意在允许用户代理按需对超时进行填充,以优化设备的功耗。例如某些处理器存在低功耗模式,定时器粒度降低;在此类平台上,用户代理可以降低定时器速率,无须一直维持高精度,从而节省耗电。

    4. 在与 idle-task 任务源关联的队列中 排队一个任务,该任务执行调用空闲回调超时算法,传递handlewindow为参数。
注意

requestIdleCallback() 仅调度单个回调,并在单个空闲周期执行。如果回调未能在指定deadline前完成其工作,则应再次调用requestIdleCallback()(可在回调中发起),以调度新的后续回调,并立即退出以将控制权归还事件循环

4.2 cancelIdleCallback() 方法

cancelIdleCallback() 方法用于取消此前登记的空闲回调。当调用 cancelIdleCallback(handle) 时,用户代理必须执行如下步骤:

  1. window为当前Window 对象。
  2. window空闲请求回调列表可运行空闲回调列表 中查找与handle相关联的条目。
  3. 若存在该条目,则从window空闲请求回调列表可运行空闲回调列表中均移除此条目。
注意

cancelIdleCallback() 可能针对window空闲请求回调列表可运行空闲回调列表中的一个条目被调用。无论哪种情况,该条目都应被移除,使回调不会被执行。

4.3 IdleDeadline 接口

每个IdleDeadline都有一个关联的获取截止时间 算法,返回DOMHighResTimeStamp ,表示该截止时间的绝对毫秒时间。截止时间初始为零。

IdleDeadline对象上调用timeRemaining() 方法时,必须返回截止前剩余时长,类型为DOMHighResTimeStamp;该值能满足测量需求同时避免计时攻击——详见 [HR-TIME] 的“隐私与安全”章节。其值按以下步骤计算:

  1. nowDOMHighResTimeStamp,表示当前高精度时间(毫秒)。
  2. deadline为调用IdleDeadline获取截止时间算法的结果。
  3. timeRemainingdeadlinenow
  4. timeRemaining为负,则置为0。
  5. 返回timeRemaining

每个 IdleDeadline都有关联的超时标记,其初始值为 false。 didTimeout getter 必须 返回 超时标记的值。

注意

调用空闲回调超时算法 超时标记设为 true,以表明该回调因超过登记时提供的 IdleRequestOptions的 timeout 属性而在非空闲周期执行。

5. 处理过程

5.1 启动空闲期算法

Window windowgetDeadline(一个返回 DOMHighResTimeStamp 的算法)启动空闲期

注意

该算法由 事件循环处理模型 判断 事件循环 处于空闲时调用。

  1. 用户代理可选地认为空闲期应当延迟时,直接返回本算法。
    注意

    为允许用户代理根据需要延迟空闲期的启动以优化设备电源。例如,若 Documentvisibility state 为 "hidden" ,则用户代理可限制空闲期的生成,例如每 10 秒 1 次以优化电源消耗。

  2. pending_listwindow空闲请求回调列表
  3. run_listwindow可运行空闲回调列表
  4. pending_list 中所有条目按顺序追加到 run_list 末尾。
  5. 清空 pending_list
  6. 在与 idle-task 任务源 关联的队列中 排队一个任务,该任务执行 调用空闲回调算法,参数为 windowgetDeadline
注意

当���时刻到截止时间的这段时间称为 空闲期。对于任意 Window,同一时刻仅有一个空闲期处于激活状态。若用户代理判断不再空闲则空闲期可提前结束,此时新空闲期需等待到达 deadline 后才能重新开始。

5.2 调用空闲回调算法

Window windowgetDeadline(返回 DOMHighResTimeStamp 的算法),调用空闲回调算法

  1. 若用户代理因新调度的高优先级工作判断应提前结束空闲期,则直接返回本算法。
  2. now 为当前时间。
  3. now 小于 getDeadline 的结果,且 window可运行空闲回调列表 非空:
    1. window可运行空闲回调列表 弹出最上方的 callback
    2. deadlineArg 为新建 IdleDeadline,其 获取截止时间算法为 getDeadline
    3. 调用 callback,传递 « deadlineArg » 和 "report"。
    4. window可运行空闲回调列表 非空,则 排队一个任务,执行本 调用空闲回调算法,参数为 getDeadlinewindow,并返回本算法
注意

即使 deadline 未到,用户代理也可在第 1 步直接返回以提前终止空闲期。例如判断更高优先级的任务已可运行时。

5.3 调用空闲回调超时算法

调用空闲回调超时算法

  1. callback 为在 window空闲请求回调列表可运行空闲回调列表 中与传递进来的 handle 相关联的条目。
  2. callback 不为 undefined:
    1. window空闲请求回调列表可运行空闲回调列表 中移除 callback
    2. now 为当前时间。
    3. deadlineArg 为新建 IdleDeadline。 将 获取截止时间 算法设为返回 now,并将 超时标记 设为 true
    4. 调用 callback,传递 « deadlineArg » 和 "report"。

6. 隐私注意事项

当调度空闲回调时,用户代理会提供其预期仍将保持空闲的估算时长。该信息可用于估算同一帧内其他应用任务及相关浏览器工作耗时。不过,开发者已可通过其他方式访问此信息——如用 requestAnimationFrame 标记帧起点,估算下一个帧的时间,并配合该信息计算任意回调内的“剩余时长”。

为减少缓存与统计指纹攻击,通过 IdleDeadline 接口返回的时间估算分辨率应根据 时间粗化跨源隔离能力调整,以确保该 API 不暴露超出其它 Web 计时器的信息。[HR-TIME]

7. 安全性注意事项

待补充

A. 致谢

编辑感谢以下人员对本规范的贡献:Sami Kyostila、Alex Clarke、Boris Zbarsky、Marcos Caceres、Jonas Sicking、Robert O'Callahan、David Baron、Todd Reifsteck、Tobin Titus、Elliott Sprehn、Tetsuharu OHZEKI、Lon Ingram、Domenic Denicola、Philippe Le Hegaret 和 Anne van Kesteren。

B. 参考文献

B.1 规范性参考文献

[HR-TIME]
高分辨率时间. Yoav Weiss. W3C. 7 November 2024. W3C 工作草案. 网址: https://www.w3.org/TR/hr-time-3/
[html]
HTML 标准. Anne van Kesteren; Domenic Denicola; Dominic Farolino; Ian Hickson; Philip Jägenstedt; Simon Pieters. WHATWG. 持续更新的标准. 网址: https://html.spec.whatwg.org/multipage/
[RFC2119]
在 RFC 中用于指示要求级别的关键词. S. Bradner. IETF. March 1997. 最佳当前实践. 网址: https://www.rfc-editor.org/rfc/rfc2119
[RFC8174]
RFC 2119 关键词中大写与小写的歧义. B. Leiba. IETF. May 2017. 最佳当前实践. 网址: https://www.rfc-editor.org/rfc/rfc8174
[webidl]
Web IDL 标准. Edgar Chen; Timothy Gu. WHATWG. 持续更新的标准. 网址: https://webidl.spec.whatwg.org/

B.2 资料性参考文献

[dom]
DOM 标准. Anne van Kesteren. WHATWG. 持续更新的标准. 网址: https://dom.spec.whatwg.org/
[RESPONSETIME]
人机对话事务中的响应时间. Robert B. Miller. December 1968. Fall Joint Computer Conference. 网址: http://yusufarslan.net/sites/yusufarslan.net/files/upload/content/Miller1968.pdf