1. 简介
Container Timing API 可用于监测 DOM 中已标注的部分何时显示在屏幕上并
完成其初始绘制。开发者可以使用 containerTiming
属性标记 DOM 的子部分(类似于 Element Timing API 的 elementtiming),并在该部分
首次完成绘制时接收性能条目。
此 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( '容器已绘制:' , entry. identifier, '时间' , entry. startTime, '大小:' , entry. size); } }); observer. observe({ entryTypes: [ "container" ] }); </ script >
应在元素添加到文档之前设置该属性(在 HTML 中设置,或者通过 JavaScript 设置时,应在将元素添加到文档之前设置)。事后设置该属性将只能捕获 后续事件和未来的绘制。
3.1. 忽略子树
containerTimingIgnore
属性忽略 DOM 树的部分内容:
< div containertiming = "foobar" > < main > ...</ main > <!-- aside 的更新不会触发容器计时事件 --> < aside containertimingignore > ...</ aside > </ div >
4. 术语
容器根是一个
具有 HTMLElement
的 containerTiming
属性。
被忽略的子树是一个
以具有 HTMLElement
的 containerTimingIgnore
属性的元素为根的子树。
已绘制区域是一个
区域(矩形的集合),表示自首次
观察以来容器根所有已绘制部分的累积,以 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 ; [Default ]object (); };toJSON 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 值。
当调用 toJSON()
时,运行默认 toJSON 步骤。
注: 用户代理需要维护容器根记录映射,以使被移除的内容不会
引入内存泄漏。具体而言,它可以将条目的生命周期与指向这些 HTMLElement
的弱指针相关联,
以便在这些 HTMLElement
被移除后的某个时间清理它们。由于该映射不会暴露给 Web 开发者,因此这不会暴露垃圾回收的时机。
6. 处理模型
注: 实现容器计时 API 的用户代理
需要在 supportedEntryTypes
中为 Window
上下文包含 "container"。
这允许开发者检测对容器计时的支持。
6.1. 每文档状态
对于每个 Document,
用户代理必须维护一个 容器根记录映射,它将 容器根 HTMLElement
映射到 容器计时记录对象。
6.2. 对 DOM 规范的修改
一旦 [DOM] 规范完成修改,本节将被移除。
我们按如下方式修改 事件分派算法。
紧接在步骤 1 之后,我们添加以下步骤:
6.3. 对 HTMLElement
接口的扩展
一旦 [DOM] 规范完成修改,本节将被移除。
我们按如下方式扩展 HTMLElement
接口:
partial interface HTMLElement { [CEReactions ,Reflect ]attribute DOMString containerTiming ; [CEReactions ,Reflect ]attribute boolean containerTimingIgnore ; };
containerTiming 属性是一个 DOMString,
它将该元素标识为一个 容器根。该值会成为相应 identifier
在对应 PerformanceContainerTiming
条目中的值。
containerTimingIgnore 属性在
存在时,将该元素及其后代标记为一个 忽略的子树,该子树不应计入祖先
容器根的容器计时测量。
6.4. 容器计时记录
本规范定义了处理模型所使用的内部数据结构:
-
一个 paintTimingInfo,它是一个 绘制计时信息。
-
一个 identifier,它是一个
DOMString。 -
一个 paintedRegion,它是一个 已绘制区域, 初始为空。
-
一个 lastNewPaintedAreaPaintTimingInfo, 它是一个 绘制计时信息,初始未设置。
-
一个 lastNewPaintedAreaElement, 它是一个
HTMLElement或 null,初始为 null。 -
一个 lastNewPaintedAreaSize, 它是一个数值,初始为 0。
-
一个 hasPendingChanges, 它是一个布尔值,初始为 false。
DOMString
identifier 的情况下创建一个
容器计时记录,执行以下步骤:
-
令 record 为一个新的 容器计时记录。
-
将 record 的 paintTimingInfo 设置为 paintTimingInfo。
-
将 record 的 identifier 设置为 identifier。
-
返回 record。
6.5. 注册容器根
当一个带有 HTMLElement
的 containerTiming
内容属性连接到文档时:
6.6. 移除 containertiming 属性
containerTiming
内容属性的 HTMLElement
element 中移除该属性时,执行以下步骤:
注:如果随后又将 containerTiming
属性重新添加到同一元素,则会在下一次绘制时创建一个新的 容器计时记录。
已绘制区域会从头开始——不会保留先前的绘制数据。
6.7. 断开容器根
HTMLElement
element 与文档断开连接时,执行以下步骤:
注:如果该元素重新连接到文档
时仍具有 containerTiming
属性,则会将其视为一次新的 容器根注册。一个新的 容器计时记录将在
下一次绘制时创建,并具有一个全新的 已绘制区域。
6.8. 处理用于容器计时的元素绘制
Document
document、一个 绘制计时信息 paintTimingInfo、一个
HTMLElement
element 和一个 DOMRectReadOnly
intersectionRect,执行以下步骤:
-
令 window 为 document 的 相关全局对象。
-
如果 element 不会计入容器计时,则返回。
-
令 containerRoot 为给定 element 后获取 容器根元素的结果。
-
如果 containerRoot 为 null,则返回。
-
令 record 为 document 的 容器根 记录映射中对应于 containerRoot 的条目。如果不存在这样的条目,则将 record 设置为给定 paintTimingInfo 和 containerRoot 的
containerTiming内容属性值后,创建容器计时记录的结果; 然后将(containerRoot → record)添加到 document 的 容器根记录映射中。 -
令 enclosingRect 为包含 intersectionRect 的最小外接矩形。
-
对于 record,给定 document、containerRoot、 element、enclosingRect 和 paintTimingInfo,可能更新最后新增绘制 区域。
-
将该
Document标记为具有待处理的容器计时更改。
注:对于每个进行绘制的图像或文本 节点,都会调用此算法。交叉矩形应使用 交叉矩形算法计算,其中以该元素作为 目标、以视口作为根,并与可视视口相交。对于文本节点,交叉 矩形是包含 拥有的文本节点集合中所有文本节点边框盒的最小矩形,并与可视视口相交。
注:容器计时旨在测量内容的 初始渲染。一旦用户滚动页面或与文档交互,就不再记录 绘制,因为这些绘制很可能是滚动或交互的结果(例如,内容 被滚动到视图中或通过点击显示),而不是页面加载的结果。当前 帧中已经待处理的条目仍会被发出。
6.9. 发出容器计时条目
Document
document 发出容器计时条目时,执行以下步骤。这应在每帧所有绘制
操作处理完毕后调用一次:
-
如果 document 没有待处理的容器计时更改,则返回。
-
对于 document 的 容器根记录映射中的每个 containerRoot → record:
-
如果 record 的 hasPendingChanges 为 false,则继续。
-
给定 record 和 containerRoot,创建一个容器计时条目。
-
将 record 的 hasPendingChanges 设置为 false。
-
将 record 的 lastNewPaintedAreaElement 设置为 null。
-
将 record 的 lastNewPaintedAreaSize 设置为 0。
-
-
将该
Document标记为不再具有待处理的容器计时更改。
注:与其他一些可能针对每个已绘制元素发出
多个条目的绘制计时 API 不同,容器计时会为每个 容器根累积已绘制区域,并且每个
容器根每帧最多发出一个 PerformanceContainerTiming
条目。这种批处理方法效率更高,并提供
容器绘制状态的整体视图。
6.10. 获取父容器根 Element
HTMLElement
element 的情况下,获取
父容器根元素,执行以下步骤:
-
令 parent 为 element 的 parentElement。
-
如果 parent 为 null,则返回 null。
-
返回给定 parent 后获取容器根元素的结果。
6.11. 计入容器计时
如果以下所有条件均为 true,则一个 HTMLElement
会计入一个 容器根的容器计时:
HTMLElement
element 是否计入一个 容器根 containerRoot 的容器计时:
-
如果 element 为 null,则返回 false。
-
如果 element 位于影子树中,则返回 false。
-
如果 element 不是 containerRoot 的后代,则返回 false。
-
如果 element 位于一个 忽略的子树中,则返回 false。
-
返回 true。
6.12. 获取容器根 Element
HTMLElement
element 的情况下,获取
容器根元素,执行以下步骤:
-
如果 element 为 null,则返回 null。
-
如果 element 的
containerTiming内容属性存在,则返回 element。 -
如果 element 的 parentElement 不为 null,则返回给定 element 的 parentElement 后获取 容器根元素的结果。
-
返回 null。
6.13. 可能更新最后新增绘制区域
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 的 容器根记录映射中。 -
对于 parentRecord,给定 document、 parentContainerRoot、element、enclosingRect 和 paintTimingInfo,可能更新最后新增绘制 区域。
注:lastPaintedElement
是在单个渲染帧中贡献了最大新增绘制区域的元素。这避免了
对实现特定绘制顺序的依赖。lastPaintedElement
旨在作为调试辅助工具,帮助开发者调查大型或复杂 容器根(例如
表格)上的更新由什么驱动,而不是作为其自身独立的性能指标。lastNewPaintedAreaSize 会在
条目发出后重置,以便每一帧的比较都从头开始。
注:此算法会报告已绘制
区域的任何变化,而不考虑其大小。即使已绘制区域只有 1 像素的变化,也会导致一个新的 PerformanceContainerTiming
条目被排入队列。希望过滤掉较小变化的开发者可以通过比较 size
在各条目之间的值来实现。
6.14. 创建容器计时条目
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
-
-
将 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
-
Bas Schouten