1. 简介
容器计时 API 可监控 DOM 中带注解的部分何时显示在屏幕上并完成其初始绘制。开发者可以使用 [^containertiming^]
属性标记 DOM 的子部分(类似于元素计时 API 的 elementtiming),并在该部分首次完成绘制时接收性能条目。
此 API 允许开发者测量页面中各类组件的计时。随着开发者 越来越多地将应用组织成组件,对应用或网页的 子区段进行性能测量的需求也在增长。
与 Element Timing 不同,渲染器无法知道 DOM 的某个区段何时已经完成
绘制(可能还会有未来的变化、用于新图像的异步请求、加载缓慢的按钮等),
因此此 API 会在发生更新时,以 PerformanceEntry
对象的形式发出候选项。
2. 动机
开发者希望测量 DOM 子区段何时被绘制,例如表格、小部件或其他 组件,以便跟踪绘制时间并将其提交给分析系统。当前的 Web API 无法充分 支持这一点:
-
Element Timing 所能支持的内容有限,不能用于整个 区段。
-
最大内容绘制(LCP)不足以用于计时页面特定部分 何时加载完成。
-
用户空间 polyfill 存在显著缺点,例如:
-
需要在绘制前标记元素(这需要服务端更改或阻塞 渲染)
-
需要 MutationObserver 来捕获新注入的元素
-
需要在文档头部运行,从而增加首次绘制时间
-
与浏览器内建 2D 引擎相比,在矩形跟踪方面可能效率更低
-
Web 作者比任何人都更了解自己的领域,并希望以其用户或组织能够理解的方式 传达自身内容块的性能(例如 “首次推文时间”)。
2.1. 生命周期
在此示例生命周期中,一个组件会在不同时间绘制多块内容,其中每一次 都会生成一个新的PerformanceContainerTiming
条目,并包含更新后的信息。
不过,一旦某个区域被绘制,该同一区域的后续绘制将不会生成新条目。
3. 用法示例
下面的示例演示如何注册一个容器根并观察其绘制计时。
< div containertiming = "foobar" > < main > ...</ main > < aside > ...</ aside > </ div > < script > const observer= new PerformanceObserver(( list) => { let perfEntries= list. getEntries(); for ( const entryof perfEntries) { console. log( '容器已绘制:' , entry. identifier, '时间' , entry. startTime, '大小:' , entry. size); } }); observer. observe({ entryTypes: [ "container" ] }); </ script >
应在元素添加到文档之前设置该属性(在 HTML 中设置,或者通过 JavaScript 设置时,应在将元素添加到文档之前设置)。事后设置该属性将只能捕获 后续事件和未来的绘制。
3.1. 忽略子树
< div containertiming = "foobar" > < main > ...</ main > <!-- aside 的更新不会触发容器计时事件 --> < aside containertimingignore > ...</ aside > </ div >
4. 术语
容器根是一个具有
[^containertiming^] 属性的 HTMLElement。
忽略的子树是一个
以具有 [^containertimingignore^] 属性的 HTMLElement
为根的子树。
绘制区域是一个
区域(矩形集合),表示自首次观测以来 容器根所有已绘制部分的累积范围,以 CSS 像素表示。绘制区域在视口坐标空间中维护。如果
容器根元素发生移动
(例如,由于布局变化或调用 moveBefore()),
先前累积的矩形不会被调整——该区域会继续在视口坐标中扩展。
容器计时 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 HTMLElement ?lastPaintedElement ;readonly attribute HTMLElement ?rootElement ; };PerformanceContainerTiming includes PaintTimingMixin ;
注: intersectionRect
的
坐标和 size
均以 CSS 像素表示(对于 size,则为
CSS 像素的平方),
这与绘制
区域的坐标空间以及交叉矩形算法所生成的单位一致。
PerformanceContainerTiming
对象都具有以下关联概念:
-
一个 标识符,初始设置为空字符串。
-
一个 intersectionRect,初始设置为一个所有值均设置为 0 的
DOMRectReadOnly。 -
一个 size,初始设置为 0。
-
一个 firstRenderTime,初始设置为 0。
-
一个 renderTime,初始设置为 0。
-
一个 lastPaintedElement,其中包含关联的
HTMLElement, 初始设置为null。 -
一个 rootElement,其中包含作为容器根的关联
HTMLElement, 初始设置为null。
entryType
属性的获取器必须返回 DOMString
"container"。
name
属性的获取器必须返回空字符串。
duration
属性必须返回 0。
startTime
属性的获取器必须返回 this 的 renderTime 值。
identifier 属性
必须返回 this 的
标识符值。
intersectionRect
属性必须返回 this 的
intersectionRect 值。
firstRenderTime
属性必须返回 this 的
firstRenderTime 值。
lastPaintedElement
属性必须返回 this 的
lastPaintedElement 值。
rootElement 属性
必须返回 this 的
rootElement 值。
注: 用户代理需要维护容器根记录映射,以使被移除的内容不会
引入内存泄漏。具体而言,它可以将条目的生命周期与指向这些 HTMLElement
的弱指针相关联,
以便在这些 HTMLElement
被移除后的某个时间清理它们。由于该映射不会暴露给 Web 开发者,因此这不会暴露垃圾回收的时机。
6. 处理模型
注: 实现容器计时 API 的用户代理
需要在 supportedEntryTypes
中为 Window
上下文包含 "container"。
这允许开发者检测对容器计时的支持。
6.1. 每文档状态
对于每个 Document,
用户代理必须维护一个容器根记录映射,该映射将容器根 HTMLElement
映射到容器计时记录对象。
6.2. 对 HTMLElement
接口的扩展
一旦 [DOM] 规范完成修改,本节将被移除。
我们按如下方式扩展 HTMLElement
接口:
partial interface HTMLElement { [CEReactions ,Reflect ]attribute DOMString containerTiming ; [CEReactions ,Reflect ]attribute boolean containerTimingIgnore ; };
containerTiming 属性是一个 DOMString,
用于将该元素标识为容器根。其值会成为相应 identifier
在对应 PerformanceContainerTiming
条目中的值。
当 containerTimingIgnore 属性
存在时,会将该元素及其后代标记为忽略的子树,该子树不应计入祖先容器根的容器计时测量。
6.3. Container Timing Record
本规范定义了处理模型使用的内部数据结构:
-
一个 paintTimingInfo,它是一个 绘制计时信息。
-
一个 identifier,它是一个
DOMString。 -
一个 paintedRegion,它是一个绘制区域, 初始为空。
-
一个 lastNewPaintedAreaPaintTimingInfo, 它是一个绘制计时信息,初始未设置。
-
一个 lastNewPaintedAreaElement,它 是一个
HTMLElement或 null,初始为 null。 -
一个 lastNewPaintedAreaSize,它是一个 数值,初始为 0。
-
一个 hasPendingChanges,它 是一个布尔值,初始为 false。
DOMString
identifier,要创建一个
容器计时记录,执行以下步骤:
-
令 record 为一个新的容器计时记录。
-
将 record 的 paintTimingInfo 设置为 paintTimingInfo。
-
将 record 的 identifier 设置为 identifier。
-
返回 record。
6.4. 注册容器根
当一个具有 [^containertiming^] 内容属性的 HTMLElement
连接到文档时:
6.5. 移除 containertiming 属性
HTMLElement
element 中移除 [^containertiming^] 内容属性时,执行以下步骤:
注:如果之后将 [^containertiming^] 属性 重新添加到同一元素,则会在下一次绘制时创建一个新的容器计时记录。已绘制区域会重新开始——不会保留先前的绘制数据。
6.6. 断开容器根
HTMLElement
element 与文档断开连接时,执行以下步骤:
注:如果该元素在仍具有 [^containertiming^] 属性时 重新连接到文档,则会将其视为一次新的容器根注册。下一次绘制时会创建一个新的容器计时记录,并具有一个全新的已绘制区域。
6.7. 为容器计时处理元素绘制
Document
document、一个绘制计时信息 paintTimingInfo、一个
HTMLElement
element 和一个 DOMRectReadOnly
intersectionRect,执行以下步骤:
-
如果 element 不为容器计时作出贡献,则返回。
-
令 containerRoot 为给定 element 时获取容器根 元素的结果。
-
如果 containerRoot 为 null,则返回。
-
令 record 为 document 的容器根 记录映射中对应于 containerRoot 的条目。如果不存在这样的条目,则将 record 设置为给定 paintTimingInfo 和 containerRoot 的 [^containertiming^] 内容属性值时创建容器计时记录的结果; 然后将 (containerRoot → record) 添加到 document 的容器根记录映射中。
-
令 enclosingRect 为 intersectionRect 的最小外接矩形。
-
给定 document、containerRoot、 element、enclosingRect 和 paintTimingInfo,为 record酌情更新最后新增的绘制 区域。
-
将该
Document标记为具有待处理的容器计时更改。
注: 此算法会针对每个进行绘制的图像或文本 节点调用。交叉矩形应使用交叉矩形算法计算,其中将元素作为 目标,将视口作为根,并与可视视口相交。对于文本节点,交叉 矩形是包含所拥有文本节点的集合中所有文本节点的边框框的最小矩形,并与可视视口相交。
6.8. 发出容器计时条目
Document
document 发出容器计时条目时,执行以下步骤。应在处理完所有绘制
操作后,每帧调用一次:
-
如果 document 不存在待处理的容器计时更改,则返回。
-
对于 document 的容器根记录映射中的每个 containerRoot → record:
-
如果 record 的 hasPendingChanges 为 false,则继续。
-
给定 record 和 containerRoot,创建容器计时条目。
-
将 record 的 hasPendingChanges 设置为 false。
-
将 record 的 lastNewPaintedAreaElement 设置为 null。
-
将 record 的 lastNewPaintedAreaSize 设置为 0。
-
-
将该
Document标记为不再存在待处理的容器计时更改。
注:与其他一些可能为每个已绘制元素发出
多个条目的绘制计时 API 不同,容器计时会为每个容器根累积已绘制区域,并且每个容器根每帧至多发出一个 PerformanceContainerTiming
条目。这种批处理方式效率更高,并可提供容器绘制状态的整体视图。
6.9. 获取父容器根 Element
HTMLElement
element,要获取
父容器根元素,执行以下步骤:
-
令 parent 为 element 的 parentElement。
-
如果 parent 为 null,则返回 null。
-
返回给定 parent 时获取容器根元素的结果。
6.10. 对容器计时产生贡献
如果以下条件全部为真,则一个 HTMLElement
会为容器根的容器计时作出贡献:
HTMLElement
element 是否为容器根
containerRoot 的容器计时作出贡献:
-
如果 element 为 null,则返回 false。
-
如果 element 在影子树中,则返回 false。
-
如果 element 不是 containerRoot 的后代,则返回 false。
-
如果 element 在忽略的子树内,则返回 false。
-
返回 true。
6.11. 获取容器根 Element
HTMLElement
element,要获取
容器根元素,执行以下步骤:
-
如果 element 为 null,则返回 null。
-
如果 element 的 [^containertiming^] 内容属性存在,则返回 element。
-
如果 element 的 parentElement 不为 null,则返回给定 element 的 parentElement 时获取 容器根元素的结果。
-
返回 null。
6.12. 可能更新最后一个新绘制区域
Document
document、一个容器根
HTMLElement
containerRoot、一个 HTMLElement
element、一个 DOMRectReadOnly
enclosingRect 和一个绘制计时信息 paintTimingInfo,
执行以下步骤:
-
令 paintedRegion 为 record 的 paintedRegion。
-
如果 paintedRegion 完全包含 enclosingRect,则返回。
-
令 newPaintedArea 为 enclosingRect 中尚未 包含在 paintedRegion 中的面积。
-
将 record 的 paintedRegion 设置为 paintedRegion 与 enclosingRect 的并集。
-
将 record 的 lastNewPaintedAreaPaintTimingInfo 设置为 paintTimingInfo。
-
如果 newPaintedArea 大于 record 的 lastNewPaintedAreaSize:
-
将 record 的 lastNewPaintedAreaElement 设置为 element。
-
将 record 的 lastNewPaintedAreaSize 设置为 newPaintedArea。
-
-
将 record 的 hasPendingChanges 设置为 true。
-
如果 containerRoot 的 [^containertimingignore^] 内容属性存在,则返回。
-
令 parentContainerRoot 为以 containerRoot 为参数获取父容器根元素的结果。
-
如果 parentContainerRoot 为 null,则返回。
-
令 parentRecord 为 document 的容器根 记录映射中对应于 parentContainerRoot 的条目。如果不存在这样的条目,则将 parentRecord 设置为以 paintTimingInfo 和 parentContainerRoot 的 [^containertiming^] 内容属性值为参数创建容器计时记录的结果;然后将 (parentContainerRoot → parentRecord) 添加到 document 的容器根记录映射中。
-
给定 document、 parentContainerRoot、element、enclosingRect 和 paintTimingInfo,为 parentRecord 可能更新最后一个新绘制 区域。
注:lastPaintedElement
是在单个渲染帧中贡献最大新绘制面积的元素。这可以避免
依赖于特定于实现的绘制顺序。lastPaintedElement
旨在作为调试辅助工具,供开发者调查是什么驱动了大型或复杂容器根(例如
表格)的更新,而不是作为独立的性能指标。发出条目后会重置lastNewPaintedAreaSize,
以便每一帧的比较都重新开始。
注:此算法会报告已绘制
区域的任何变化,而不论其大小。即使已绘制面积仅发生 1 像素的变化,也会导致一个新的 PerformanceContainerTiming
条目被排入队列。希望过滤较小变化的开发者可以通过比较条目之间的 size
值来实现。
6.13. 创建容器计时条目
HTMLElement
containerRoot 的情况下创建容器计时条目,用户代理必须执行以下步骤:
-
令entry为一个新的
PerformanceContainerTiming条目,并将其:-
entryType设置为“container” -
name设置为空字符串 -
startTime设置为record的lastNewPaintedAreaPaintTimingInfo的paintTime -
duration设置为 0 -
identifier设置为record的identifier -
firstRenderTime设置为record的paintTimingInfo的paintTime -
intersectionRect设置为record的paintedRegion的边界矩形 -
size设置为record的paintedRegion的总面积,以 CSS 像素的平方为单位 -
lastPaintedElement设置为record的lastNewPaintedAreaElement -
rootElement设置为containerRoot
-
-
将 PerformanceEntry 加入队列entry。
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
-
Bas Schouten