1. 简介
在页面加载时以及用户随后与页面交互时,应用程序和浏览器都会排队各种事件,然后由浏览器执行这些事件——例如,用户代理根据用户活动安排输入事件,应用程序安排 requestAnimationFrame 的回调以及其他回调等。一旦进入队列,浏览器会逐个出队并执行这些事件。
然而,有些任务可能需要很长时间(多个帧),如果/当这种情况发生时,UI 线程可能会被阻塞,并且也会阻塞所有其他任务。对于用户来说,这通常表现为页面“卡住”,浏览器无法响应用户输入;这是当今网页上用户体验不佳的主要原因之一:
- 延迟“可交互时间”:
-
在页面加载时,甚至完全视觉渲染后,长任务通常会占用主线程并阻止用户与页面交互。设计不良的第三方内容通常是罪魁祸首。
- 高/不稳定的输入延迟:
-
关键用户交互事件(例如点击、滚动、滑动等)排在长任务之后,导致用户体验卡顿且不可预测。
- 高/不稳定的事件处理延迟:
-
与输入类似,处理事件回调(例如 onload 事件等)会延迟应用程序更新。
- 卡顿的动画和滚动:
-
某些动画和滚动交互需要合成器线程与主线程之间的协调;如果长任务阻塞了主线程,则会影响动画和滚动的响应性。
一些应用程序(以及 RUM 供应商)已经尝试识别和跟踪发生“长任务”的情况。例如,一个已知的模式是安装一个短周期定时器并检查连续到期之间的时间间隔:如果时间间隔大于定时器周期,则很可能一个或多个长任务延迟了事件循环的执行。这种方法大多有效,但有几个性能上的负面影响:通过轮询检测长任务,应用程序阻止了静止状态和长时间的空闲块(参见 requestIdleCallback);这对电池寿命不利;无法知道导致延迟的原因(例如第一方或第三方代码)。
RAIL 性能模型建议应用程序应在不到 100 毫秒内响应用户输入(对于触摸移动和滚动,阈值为 16 毫秒)。此 API 的目标是提供有关可能阻止应用程序达到这些目标的任务的通知。此 API 提供了耗时 50 毫秒或更长时间的任务通知。没有这些任务的网站应该能够在 100 毫秒内响应用户输入:完成用户输入时正在执行的任务需要不到 50 毫秒,响应用户输入的任务需要不到 50 毫秒。
1.1. 使用示例
const observer= new PerformanceObserver( function ( list) { for ( const entryof list. getEntries()) { // 处理长任务通知: // 回传分析和监控 // ... } }); // 为之前和未来的长任务通知注册观察者。 observer. observe({ type: "longtask" , buffered: true }); // 此后的长脚本执行将导致排队 // 并在观察者中接收“longtask”条目。
2. 术语
长任务是指持续时间超过50毫秒的以下任意情况:
浏览上下文容器对于浏览上下文 bc是bc的活动文档的节点可导航的容器。
注:此术语已过时,在修订时应重用新术语。
罪魁祸首浏览上下文容器是指整体上被认定为长任务的浏览上下文容器(iframe
、
object
等)。
归因是指识别对长任务产生重大影响的工作类型(如脚本、布局等),以及识别负责该工作的罪魁祸首浏览上下文容器。
3. 长任务计时
长任务计时涉及以下新接口:
3.1.
PerformanceLongTaskTiming
接口
[Exposed =Window ]interface :
PerformanceLongTaskTiming PerformanceEntry { /* 重载PerformanceEntry */readonly attribute DOMHighResTimeStamp ;
startTime readonly attribute DOMHighResTimeStamp ;
duration readonly attribute DOMString ;
name readonly attribute DOMString ;
entryType readonly attribute FrozenArray <TaskAttributionTiming >attribution ; [Default ]object (); };
toJSON
PerformanceLongTaskTiming
属性的值在§ 4.1 报告长任务的处理模型中设置。以下提供了如何设置它们的信息性摘要。
name
属性的getter将返回以下字符串之一:
- "
unknown
" -
长任务源于用户代理在事件循环之外执行的工作。
- "
self
" - "
same
"- origin- ancestor - "
same
"- origin- descendant - "
same
"- origin - "
cross
"- origin- ancestor - "
cross
"- origin- descendant - "
cross
"- origin- unreachable - "
multiple
"- contexts
注:这些名称存在一些不一致性,例如"-unreachable"和"-contexts"后缀。保留这些名称是出于向后兼容性的原因。
entryType
属性的getter步骤是返回
。
startTime
属性的getter步骤是返回任务开始时的DOMHighResTimeStamp
。
duration
属性的getter步骤是返回等于任务开始和结束之间经过时间的DOMHighResTimeStamp
,精度为1毫秒。
attribution
属性的getter将返回TaskAttributionTiming
条目的冻结数组。
3.2.
TaskAttributionTiming
接口
[Exposed =Window ]interface :
TaskAttributionTiming PerformanceEntry { /* 重载PerformanceEntry */readonly attribute DOMHighResTimeStamp ;
startTime readonly attribute DOMHighResTimeStamp ;
duration readonly attribute DOMString ;
name readonly attribute DOMString ;
entryType readonly attribute DOMString containerType ;readonly attribute DOMString containerSrc ;readonly attribute DOMString containerId ;readonly attribute DOMString containerName ; [Default ]object (); };
toJSON
TaskAttributionTiming
属性的值在§ 4.1 报告长任务的处理模型中设置。以下提供了如何设置它们的信息性摘要。
name
属性的getter始终返回"unknown
"。
entryType
属性的getter始终返回"taskattribution
"。
startTime
属性的getter始终返回0。
duration
属性的getter始终返回0。
containerType
属性的getter将返回罪魁祸首浏览上下文容器的类型,如"iframe
"、"embed
"或"object
"。如果未找到单一罪魁祸首浏览上下文容器,则返回"window
"。
containerName
属性的getter将返回容器的name
内容属性值。如果未找到单一罪魁祸首浏览上下文容器,则返回空字符串。
containerId
属性的getter将返回容器的id
内容属性值。如果未找到单一罪魁祸首浏览上下文容器,则返回空字符串。
containerSrc
属性的getter将返回容器的src
内容属性值。如果未找到单一罪魁祸首浏览上下文容器,则返回空字符串。
3.3. 指向罪魁祸首
本节为非规范性内容。
一个长任务可能涉及不同类型的工作(如脚本、布局、样式等),并且可能在不同的浏览上下文中执行,或者它可能是全局性的,例如跨整个代理集群或浏览上下文组集的长时间垃圾回收。
因此,归因有几个方面:
-
指向长任务涉及的工作类型,以及其关联的罪魁祸首浏览上下文容器:这体现在
TaskAttributionTiming
对象中,位于attribution
字段(属于PerformanceLongTaskTiming
)。
因此,name
字段和attribution
字段共同描述了长任务归因于何处。在传递这些信息时,必须遵守Web的同源策略。
这些字段不是独立的。下表概述了它们之间的关系:
name
|
由
attribution
牵涉的
罪魁祸首浏览上下文容器(Culprit browsing context
container)
|
---|---|
"self "
| 空 |
"same-origin-ancestor "
| 同源罪魁 |
"same-origin-descendant "
| 同源罪魁 |
"same-origin "
| 同源罪魁 |
"cross-origin-ancestor "
| 空 |
"cross-origin-descendant "
| 空 |
"cross-origin-unreachable "
| 空 |
"multiple-contexts "
| 空 |
"unknown "
| 空 |
4. 处理模型
注:实现 Long Tasks API 的用户代理需要在
中包含 supportedEntryTypes
,
针对 Window
上下文。
这允许开发者检测是否支持长任务。
4.1. 报告长任务
-
如果 end time 减去 start time 小于 50ms 的长任务阈值,则终止这些步骤。
-
令 destinationRealms 为一个空集合。
-
确定将要报告的 JavaScript Realm 集合:
对于 top-level browsing contexts 中的每个 顶级浏览上下文 topmostBC:
-
用户代理可以从 destinationRealms 中移除一些 JavaScript Realm。
注:这种移除可用于避免为用户代理在单独进程中处理的 JavaScript Realm 报告长任务。但该概念未被精确定义。
关于哪些 文档
能看到哪些长任务的范围仍在讨论中,因此该逻辑未来可能会变化。[Issue #75]
-
对于 destinationRealms 中的每个 (destinationRealm, crossOriginIsolatedCapability):
-
令 name 为空字符串。该值用于下方报告 最小罪魁归因。
-
令 culpritSettings 为
。null -
处理 task 的 脚本执行环境设置对象集,以如下方式确定 name 和 culpritSettings:
-
如果 task 的 脚本执行环境设置对象集为空:将 name 设为 "
unknown
",culpritSettings 设为
。null -
否则,如果 task 的 脚本执行环境设置对象集长度大于1:将 name 设为 "
multiple-contexts
",culpritSettings 设为
。null -
否则(即长度为1):
-
将 culpritSettings 设为 task 的 脚本执行环境设置对象集中的唯一项。
-
令 destinationSettings 为 destinationRealm 的 相关设置对象。
-
令 destinationOrigin 为 destinationSettings 的 origin。
-
断言:culpritBC 不为
。null -
如果 culpritSettings 与 destinationSettings 相同,则将 name 设为 "
self
"。 -
否则,如果 culpritSettings 的 origin 与 destinationOrigin 同源:
-
如果 destinationBC 为
,则 name 设为 "null same-origin
"。 -
否则,如果 culpritBC 是 destinationBC 的 祖先,则 name 设为 "
same-origin-ancestor
"。 -
否则,如果 destinationBC 是 culpritBC 的 祖先,则 name 设为 "
same-origin-descendant
"。 -
否则,name 设为 "
same-origin
"。
-
-
否则:
-
如果 destinationBC 为
,则 name 设为 "null cross-origin-unreachable
"。 -
否则,如果 culpritBC 是 destinationBC 的 祖先,则 name 设为 "
cross-origin-ancestor
",并将 culpritSettings 设为
。null 注:出于安全原因不会报告此项,开发者应自行查找。
-
否则,如果 destinationBC 是 culpritBC 的 祖先,则 name 设为 "
cross-origin-descendant
"。 -
否则,name 设为 "
cross-origin-unreachable
"。
-
-
-
-
令 attribution 为一个新的
TaskAttributionTiming
对象,使用 destinationRealm 并按如下设置其属性:-
将 attribution 的
name
属性设为 "unknown
"。注:未来 API 版本会为
TaskAttributionTiming
对象的name
属性增加更多值,目前仅有一个值。 -
将 attribution 的
entryType
属性设为
。"taskattribution" -
将 attribution 的
containerType
属性设为
。"window" -
将 attribution 的
containerName
和containerSrc
属性设为空字符串。 -
如果 culpritSettings 不为
:null -
断言:culpritBC 不为
。null -
令 container 为 culpritBC 的 浏览上下文容器。
-
断言:container 不为
。null -
将 attribution 的
containerId
属性设为 container 的 ID 的值,若未设置则为空字符串。 -
如果 container 是
iframe
元素:-
将 attribution 的
containerType
属性设为 "iframe
"。 -
将 attribution 的
containerName
属性设为 container 的name
内容属性的值,若无则为空字符串。 -
将 attribution 的
containerSrc
属性设为 container 的src
内容属性的值,若无则为空字符串。
注:此处记录的是 frame 的
src
属性,而不是其当前 URL,主要用于识别 frame,允许发现跨域 iframe 的当前 URL 存在安全问题。 -
-
如果 container 是
frame
元素:-
将 attribution 的
containerType
属性设为 "frame
"。 -
将 attribution 的
containerName
属性设为 container 的name
内容属性的值,若无则为空字符串。 -
将 attribution 的
containerSrc
属性设为 container 的src
内容属性的值,若无则为空字符串。
-
-
如果 container 是
object
元素:-
将 attribution 的
containerType
属性设为 "object
"。 -
将 attribution 的
containerName
属性设为 container 的 name 内容属性的值,若无则为空字符串。 -
将 attribution 的
containerSrc
属性设为 container 的data
内容属性的值,若无则为空字符串。
-
-
如果 container 是
embed
元素:-
将 attribution 的
containerType
属性设为 "embed
"。 -
将 attribution 的
containerName
属性设为空字符串。 -
将 attribution 的
containerSrc
属性设为 container 的src
内容属性的值,若无则为空字符串。
-
-
-
创建一个新的
PerformanceLongTaskTiming
对象 newEntry,使用 destinationRealm 并按如下设置其属性:-
将 newEntry 的
name
属性设为 name。 -
将 newEntry 的
entryType
属性设为 "longtask
"。 -
将 newEntry 的
startTime
属性设为 粗化 start time 的结果,参数为 crossOriginIsolatedCapability。 -
将 newEntry 的
duration
属性设为 dur 的整数部分。 -
如果 attribution 不为
,则将 newEntry 的null attribution
属性设为包含单个值 attribution 的新冻结数组。注:未来 API 版本会为
attribution
属性增加更多值,目前仅包含一个值。
-
-
将 PerformanceEntry newEntry 入队。
-
5. 安全与隐私注意事项
Long Tasks API 遵循同源策略,仅包含关于长任务来源的同源安全归因信息。长任务有 50ms 的阈值,持续时间仅以 1 毫秒为粒度提供。这些措施共同为跨域信息泄露提供了充分保护。
Long Tasks API 提供了关于用户执行任务的持续时间和类型的计时信息,以及归因信息(如导致函数调用的浏览上下文)。这可能使攻击者能够利用侧信道计时攻击来猜测用户行为或识别用户。例如,长脚本后跟长渲染的模式可能被用来推测用户与社交组件的交互。详细的函数调用归因可用于确定用户的具体操作。
虽然该 API 没有引入新的隐私攻击,但它可能加速现有的隐私攻击。可根据需要实施如下缓解措施:
-
进一步收紧或为 API 提供的长任务持续时间添加随机扰动,使攻击更难实施。
-
限制 API 暴露长任务的来源数量,并对后续任务的归因进行混淆。例如,一个包含 5 个 iframe 的页面只对其中 3 个 iframe 的任务提供归因,其余 2 个任务则不提供归因(
name
设为unknown
)。 -
允许在达到一定阈值后丢弃罪魁/归因信息。例如,超过 10 个长任务后,所有条目都不再提供归因,其
name
设为 "unknown
"。 -
为暴露的计时信息增加内置延迟,使依赖长任务数量的攻击更难执行。
5.1. 对观察者暴露了什么?
顶级页面内的所有观察者(即页面内所有 iframe 及主框架)都会收到长任务存在的通知。我们暴露了任务的开始时间、持续时间(1ms 粒度)以及指向罪魁 frame 的指针。这些信息目前已经可以通过 setTimeout 以更高分辨率观测到。攻击者可以通过清空页面其他内容,仅添加易受攻击的跨域资源,确保 setTimeout 的延迟由该资源引起。其他页面(标签页或窗口)中的观察者不应收到通知,无论用户代理的架构如何。
跨域暴露规则:
-
跨域观察者可以看到罪魁的大致方向,例如如果罪魁是深层嵌套的 iframe,宿主页面可以看到自己与罪魁之间的第一个跨域。
-
反之,如果罪魁是顶级页面,则深层嵌套的 iframe 可以看到长任务发生在其跨域祖先,但不会获得任何详细信息。
5.2. 考虑的攻击场景
以下是已考虑的计时攻击:
-
传统计时攻击:利用外部资源加载时间揭示私有数据的大小。例如,隐藏图片的数量、用户名是否有效等。参见示例。
-
侧信道计时攻击:利用视频解析、脚本解析、App Cache 读取或 Cache API(service worker)使用的时间来唯一标识用户,或创建用户的年龄、性别、位置、兴趣等画像。例如,此文提到,社交网络的状态更新可仅限于某些人群(如 20-30 岁女性),而永久链接页面的文件大小可用于判断用户是否属于目标人群。
这些场景通过 50ms 阈值和遵守跨域边界(即不向不可信跨域观察者显示任务类型或额外归因信息)得到应对。