1. 简介
容器计时 API 能够监测 DOM 中带注解的区段何时显示在屏幕上并
完成其初始绘制。开发者可以使用 containertiming
属性(类似于 Element Timing API 的 elementtiming)来标记 DOM 的子区段,并在该区段
第一次被绘制时接收性能条目。
此 API 允许开发者测量页面中各类组件的计时。随着开发者 越来越多地将应用组织成组件,对应用或网页的 子区段进行性能测量的需求也在增长。
与 Element Timing 不同,渲染器无法知道 DOM 的某个区段何时已经完成
绘制(可能还会有未来的变化、用于新图像的异步请求、加载缓慢的按钮等),
因此此 API 会在发生更新时,以 PerformanceEntry
对象的形式发出候选项。
2. 动机
开发者希望测量 DOM 子区段何时被绘制,例如表格、小部件或其他 组件,以便跟踪绘制时间并将其提交给分析系统。当前的 Web API 无法充分 支持这一点:
-
Element Timing 所能支持的内容有限,不能用于整个 区段。
-
最大内容绘制(LCP)不足以用于计时页面特定部分 何时加载完成。
-
用户空间 polyfill 存在显著缺点,例如:
-
需要在绘制前标记元素(这需要服务端更改或阻塞 渲染)
-
需要 MutationObserver 来捕获新注入的元素
-
需要在文档头部运行,从而增加首次绘制时间
-
与浏览器内建 2D 引擎相比,在矩形跟踪方面可能效率更低
-
Web 作者比任何人都更了解自己的领域,并希望以其用户或组织能够理解的方式 传达自身内容块的性能(例如 “首次推文时间”)。
2.1. 生命周期
在此示例生命周期中,一个组件会在不同时间绘制多块内容,其中每一次 都会生成一个新的PerformanceContainerTiming
条目,并包含更新后的信息。
不过,一旦某个区域被绘制,该同一区域的后续绘制将不会生成新条目。
3. 用法示例
下面的示例演示如何注册一个容器根并观察其绘制计时。
containertiming
属性按元素进行注册:
< div containertiming = "foobar" > < main > ...</ main > < aside > ...</ aside > </ div > < script > const observer= new PerformanceObserver(( list) => { let perfEntries= list. getEntries(); for ( const entryof perfEntries) { console. log( 'Container painted:' , entry. identifier, 'at' , entry. startTime, 'size:' , entry. size); } }); observer. observe({ entryTypes: [ "container" ] }); </ script >
该属性应在元素被添加到文档之前设置(在 HTML 中设置,或者如果在 JavaScript 中设置,则应在将其添加到文档之前设置)。事后设置该属性只会捕获 后续事件和未来绘制。
3.1. 忽略子树
containertiming-ignore>
属性忽略 DOM 树的部分内容:
< div containertiming = "foobar" > < main > ...</ main > <!-- Aside updates won't trigger container timing events --> < aside containertiming-ignore > ...</ aside > </ div >
4. 术语
容器根是一个
具有 Element
且带有 containertiming
属性的元素。
被忽略的子树是一个
以带有 Element
和 containertiming-ignore>
属性的元素为根的子树。
已绘制区域是一个 区域(矩形集合),表示某个 容器根自首次 被观察以来所有已绘制部分的累积。
容器计时 API 提供关于容器根何时被绘制到屏幕上的计时信息。
5. PerformanceContainerTiming
接口
[Exposed =Window ]interface :PerformanceContainerTiming PerformanceEntry {readonly attribute DOMString identifier ;readonly attribute DOMRectReadOnly intersectionRect ;readonly attribute unsigned long long size ;readonly attribute DOMHighResTimeStamp firstRenderTime ;readonly attribute Element ?lastPaintedElement ;readonly attribute Element ?rootElement ; };PerformanceContainerTiming includes PaintTimingMixin ;
PerformanceContainerTiming
对象都有这些关联概念:
-
一个 identifier,初始设置为空字符串。
-
一个 intersectionRect,初始设置为一个所有值均为 0 的
DOMRectReadOnly。 -
一个 size,初始设置为 0。
-
一个 firstRenderTime,初始设置为 0。
-
一个 renderTime,初始设置为 0。
-
一个 lastPaintedElement,包含关联的
Element, 初始设置为null。 -
一个 rootElement,包含作为容器根的关联
Element, 初始设置为null。
entryType
属性的 getter 必须返回 DOMString
"container"。
name
属性的 getter 必须返回空字符串。
duration
属性必须返回 0。
startTime
属性的 getter 必须返回 this 的 renderTime 的值。
identifier 属性
必须返回 this 的
identifier 的值。
intersectionRect
属性必须返回 this 的
intersectionRect 的值。
firstRenderTime
属性必须返回 this 的
firstRenderTime 的值。
lastPaintedElement
属性必须返回 this 的
lastPaintedElement 的值。
rootElement 属性
必须返回 this 的
rootElement 的值。
注: 用户代理需要维护 容器根记录映射,以确保已移除的内容不会
引入内存泄漏。特别是,它可以将条目的生命周期绑定到指向这些 Element
的弱指针,从而在这些 Element
被移除之后的某个时候对其进行清理。由于该映射不会暴露给 Web 开发者,因此这不会暴露垃圾收集计时。
6. 处理模型
注: 实现容器计时 API 的用户代理
需要在 supportedEntryTypes
中为 Window
上下文包含 "container"。
这允许开发者检测对容器计时的支持。
6.1. 每文档状态
对于每个 Document,
用户代理必须维护一个 容器根记录映射,该映射将 容器根 Element
映射到 Container Timing Record 对象。
6.2. 对 Element
接口的扩展
一旦 [DOM] 规范被修改,本节将被移除。
我们按如下方式扩展 Element
接口:
partial interface Element { [CEReactions ]attribute DOMString containertiming ; [CEReactions ]attribute DOMString ?; };containertimingIgnore
containertiming 属性是一个 DOMString,
用于将元素标识为一个 容器根。该值会成为对应 identifier
在相应 PerformanceContainerTiming
条目中的值。
containertiming-ignore 属性在
存在时,会将该元素及其后代标记为一个 被忽略的子树,该子树不应对祖先 容器根 的容器计时
测量作出贡献。
6.3. Container Timing Record
本规范定义了处理模型使用的内部数据结构:
-
一个 paintTimingInfo,它是 paint timing info。
-
一个 identifier,它是一个
DOMString。 -
一个 paintedRegion,它是一个 已绘制区域, 初始为空。
-
一个 lastNewPaintedAreaPaintTimingInfo, 它是一个 paint timing info,初始未设置。
-
一个 lastNewPaintedAreaElement,它 是一个
Element或 null,初始为 null。 -
一个 hasPendingChanges,它 是一个布尔值,初始为 false。
DOMString
identifier 的情况下创建一个
Container Timing Record,执行以下步骤:
-
令 record 为一个新的 Container Timing Record。
-
将 record 的 paintTimingInfo 设置为 paintTimingInfo。
-
将 record 的 identifier 设置为 identifier。
-
返回 record。
6.4. 注册容器根
当一个带有 [^containertiming^] 内容属性的 Element
被连接到文档时:
6.5. 处理用于容器计时的元素已绘制
Document
document、一个 paint timing info paintTimingInfo、一个
Element
element 和一个 DOMRectReadOnly
intersectionRect,执行以下步骤:
-
如果 element 不对容器计时有贡献,则返回。
-
令 containerRoot 为在给定 element 的情况下执行获取 容器根元素的结果。
-
如果 containerRoot 为 null,则返回。
-
令 record 为 document 的 容器根 记录映射中针对 containerRoot 的条目。如果不存在这样的条目,则将 record 设置为在给定 paintTimingInfo 和 containerRoot 的 [^containertiming^] 内容属性值的情况下执行创建一个 Container Timing Record的结果; 然后将 (containerRoot → record) 添加到 document 的 容器根记录映射中。
-
令 enclosingRect 为 intersectionRect 的最小包围矩形。
-
对于 record,在给定 document、containerRoot、 element、enclosingRect 和 paintTimingInfo 的情况下, 可能更新最后的新绘制 区域。
-
将该
Document标记为具有待处理的容器计时更改。
注: 此算法会针对每个发生绘制的图像或文本 节点调用。交叉矩形应使用 交叉矩形算法计算,以元素作为目标、 视口作为根,并与视觉视口求交。对于文本节点,交叉矩形是包含 所属文本节点集合中所有文本节点边框盒的 最小矩形,并与视觉视口求交。
6.6. 发出容器计时条目
Document
document 发出容器计时条目时,执行以下步骤。此操作应在所有绘制
操作已处理之后每帧调用一次:
-
如果 document 没有待处理的容器计时更改,则返回。
-
对 document 的 容器根记录映射中的 每个 containerRoot → record,依次执行:
-
如果 record 的 hasPendingChanges 为 false,则继续。
-
在给定 record 和 containerRoot 的情况下,创建一个 Container Timing 条目。
-
将 record 的 hasPendingChanges 设置为 false。
-
-
将该
Document标记为不再具有待处理的容器计时更改。
注: 与一些其他可能为每个已绘制元素发出
多个条目的绘制计时 API 不同,容器计时会为每个 容器根累积已绘制区域,并且每帧每个容器根最多发出
一个 PerformanceContainerTiming
条目。这种批处理方式更高效,并能提供对容器绘制状态的整体视图。
6.7. 获取父容器根 Element
Element
element 的情况下获取
父容器根元素,执行以下步骤:
-
令 parent 为 element 的 parentElement。
-
如果 parent 为 null,则返回 null。
-
返回在给定 parent 的情况下执行获取容器根元素的结果。
6.8. 对容器计时有贡献
如果以下条件均为 true,则一个 Element
对某个容器根的容器计时有贡献:
Element
element 是否对一个 容器根 containerRoot 的容器计时有贡献:
-
如果 element 为 null,则返回 false。
-
如果 element 在影子树中,则返回 false。
-
如果 element 不是 containerRoot 的后代,则返回 false。
-
如果 element 位于某个 被忽略的子树内,则返回 false。
-
返回 true。
6.9. 获取容器根 Element
Element
element 的情况下获取
容器根元素,执行以下步骤:
-
如果 element 为 null,则返回 null。
-
如果 element 的 [^containertiming^] 内容属性存在,则返回 element。
-
如果 element 的 parentElement 不为 null,则返回在给定 element 的 parentElement 的情况下执行获取 容器根元素的结果。
-
返回 null。
6.10. 可能更新最后的新绘制区域
Document
document、一个 容器根
Element
containerRoot、一个 Element
element、一个 DOMRectReadOnly
enclosingRect 和一个 paint timing info paintTimingInfo
的情况下,为 record 可能更新最后的新绘制区域,
执行以下步骤:
-
令 paintedRegion 为 record 的 paintedRegion。
-
如果 paintedRegion 完全包含 enclosingRect,则返回。
-
将 record 的 paintedRegion 设置为 paintedRegion 与 enclosingRect 的并集。
-
将 record 的 lastNewPaintedAreaPaintTimingInfo 设置为 paintTimingInfo。
-
将 record 的 lastNewPaintedAreaElement 设置为 element。
-
将 record 的 hasPendingChanges 设置为 true。
-
如果 containerRoot 的 [^containertiming-ignore^] 内容属性存在,则返回。
-
令 parentContainerRoot 为在给定 containerRoot 的情况下执行获取父容器根元素的结果。
-
如果 parentContainerRoot 为 null,则返回。
-
令 parentRecord 为 document 的 容器根 记录映射中针对 parentContainerRoot 的条目。如果不存在这样的条目,则将 parentRecord 设置为在给定 paintTimingInfo 和 parentContainerRoot 的 [^containertiming^] 内容属性值的情况下执行创建一个 Container Timing Record的结果; 然后将 (parentContainerRoot → parentRecord) 添加到 document 的 容器根记录映射中。
-
对于 parentRecord,在给定 document、 parentContainerRoot、element、enclosingRect 和 paintTimingInfo 的情况下,可能更新最后的新绘制 区域。
注: 此算法会报告对已绘制
区域的任何更改,而不考虑大小。即使已绘制区域发生 1 像素的变化,也会导致一个新的 PerformanceContainerTiming
条目被排入队列。希望过滤掉小变化的开发者可以通过比较各条目之间的 size
值来实现。
6.11. 创建容器计时条目
Element
containerRoot 的情况下创建一个 Container Timing 条目,
用户代理必须执行以下步骤:
-
令 entry 为一个新的
PerformanceContainerTiming条目,并将其:-
entryType设置为 "container" -
name设置为空字符串 -
startTime设置为 record 的 lastNewPaintedAreaPaintTimingInfo 的paintTime -
duration设置为 0 -
identifier设置为 record 的 identifier -
firstRenderTime设置为 record 的 paintTimingInfo 的paintTime -
intersectionRect设置为 record 的 paintedRegion 的边界矩形 -
size设置为 record 的 paintedRegion 的总面积 -
lastPaintedElement设置为 record 的 lastNewPaintedAreaElement -
rootElement设置为 containerRoot
-
-
将 entry 排入 PerformanceEntry 队列。
7. 安全与隐私考量
7.1. 跨源限制
该 API 遵守跨源边界:
-
属于跨源 iframe 的元素不会暴露给父框架。
-
除非开发者通过
postMessage显式传递,否则计时信息不会跨越框架边界。
7.2. 信息暴露
此 API 提供的大多数信息都已经可以通过现有 API 进行估算:
-
Element Timing 会返回图像和文本的首次渲染时间。
-
Paint Timing API 提供相关时间戳。
-
这些 API 的组合可以近似容器计时信息,尽管效率较低。
该 API 不会暴露:
-
渲染引擎的内部实现细节
-
开发者尚无权访问的元素相关信息
-
比现有 Performance API 已可用粒度更细的计时信息
7.3. 计时攻击
该 API 使用 DOMHighResTimeStamp,
出于安全目的,它可能受到分辨率限制,这与其他 Performance
API 一致。
7.4. 隐私考量
该 API 不会:
-
支持跨站点跟踪用户
-
暴露浏览历史
-
提供超出站点通过脚本执行已经能够访问的用户行为信息
8. 致谢
非常感谢以下人士提供的宝贵反馈和建议:
-
Barry Pollard
-
Michael Mocny
-
Scott Haseley
-
Sergey Chernyshev