容器计时 API

社区组报告草案,

此版本:
https://WICG.github.io/container-timing/
测试套件:
https://github.com/web-platform-tests/wpt/tree/master/container-timing
问题跟踪:
GitHub
编辑:
Jason Williams (Bloomberg)
(Igalia)

摘要

本规范定义了一个 API,用于监测 DOM 中带注解的区段何时显示在 屏幕上并已完成其初始绘制。

本文档状态

本规范由 Web 平台孵化器 社区组发布。 它不是 W3C 标准,也不在 W3C 标准流程中。 请注意,根据 W3C 社区贡献者许可协议 (CLA), 适用有限的退出机制和其他条件。 了解更多关于 W3C 社区组和商业组的信息。

1. 简介

容器计时 API 可监控 DOM 中带注解的部分何时显示在屏幕上并完成其初始绘制。开发者可以使用 [^containertiming^] 属性标记 DOM 的子部分(类似于元素计时 API 的 elementtiming),并在该部分首次完成绘制时接收性能条目。

此 API 允许开发者测量页面中各类组件的计时。随着开发者 越来越多地将应用组织成组件,对应用或网页的 子区段进行性能测量的需求也在增长。

与 Element Timing 不同,渲染器无法知道 DOM 的某个区段何时已经完成 绘制(可能还会有未来的变化、用于新图像的异步请求、加载缓慢的按钮等), 因此此 API 会在发生更新时,以 PerformanceEntry 对象的形式发出候选项。

2. 动机

开发者希望测量 DOM 子区段何时被绘制,例如表格、小部件或其他 组件,以便跟踪绘制时间并将其提交给分析系统。当前的 Web API 无法充分 支持这一点:

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 entry of 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. 术语

容器根是一个具有 [^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 对象都具有以下关联概念:

entryType 属性的获取器必须返回 DOMString "container"

name 属性的获取器必须返回空字符串。

duration 属性必须返回 0。

startTime 属性的获取器必须返回 thisrenderTime 值。

identifier 属性 必须返回 this标识符值。

intersectionRect 属性必须返回 thisintersectionRect 值。

size 属性必须返回 thissize 值。

firstRenderTime 属性必须返回 thisfirstRenderTime 值。

lastPaintedElement 属性必须返回 thislastPaintedElement 值。

rootElement 属性 必须返回 thisrootElement 值。

注: 用户代理需要维护容器根记录映射,以使被移除的内容不会 引入内存泄漏。具体而言,它可以将条目的生命周期与指向这些 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 和一个 DOMString identifier,要创建一个 容器计时记录,执行以下步骤:
  1. record 为一个新的容器计时记录

  2. recordpaintTimingInfo 设置为 paintTimingInfo

  3. recordidentifier 设置为 identifier

  4. 返回 record

6.4. 注册容器根

当一个具有 [^containertiming^] 内容属性的 HTMLElement 连接到文档时:

  1. 用户代理必须将该元素注册为容器根

  2. 用户代理必须为该容器根初始化一个空的已绘制区域

  3. 用户代理必须跟踪容器根子树中的所有绘制操作,但不包括任何 被忽略的子树

6.5. 移除 containertiming 属性

当从 HTMLElement element 中移除 [^containertiming^] 内容属性时,执行以下步骤:
  1. documentelement节点文档

  2. 如果 document容器根记录映射包含 element 的条目, 则移除该条目。

注:如果之后将 [^containertiming^] 属性 重新添加到同一元素,则会在下一次绘制时创建一个新的容器计时记录已绘制区域会重新开始——不会保留先前的绘制数据。

6.6. 断开容器根

当一个容器根 HTMLElement element 与文档断开连接时,执行以下步骤:
  1. documentelement节点文档

  2. 如果 document容器根记录映射包含 element 的条目,则移除该条目。

注:如果该元素在仍具有 [^containertiming^] 属性时 重新连接到文档,则会将其视为一次新的容器根注册。下一次绘制时会创建一个新的容器计时记录,并具有一个全新的已绘制区域

6.7. 为容器计时处理元素绘制

当元素被绘制且用户代理需要处理容器计时更新时,给定一个 Document document、一个绘制计时信息 paintTimingInfo、一个 HTMLElement element 和一个 DOMRectReadOnly intersectionRect,执行以下步骤:
  1. 如果 element为容器计时作出贡献,则返回。

  2. containerRoot 为给定 element获取容器根 元素的结果。

  3. 如果 containerRoot 为 null,则返回。

  4. recorddocument容器根 记录映射中对应于 containerRoot 的条目。如果不存在这样的条目,则将 record 设置为给定 paintTimingInfocontainerRoot 的 [^containertiming^] 内容属性值时创建容器计时记录的结果; 然后将 (containerRootrecord) 添加到 document容器根记录映射中。

  5. enclosingRectintersectionRect 的最小外接矩形。

  6. 给定 documentcontainerRootelementenclosingRectpaintTimingInfo,为 record酌情更新最后新增的绘制 区域

  7. 将该 Document 标记为具有待处理的容器计时更改。

注: 此算法会针对每个进行绘制的图像或文本 节点调用。交叉矩形应使用交叉矩形算法计算,其中将元素作为 目标,将视口作为根,并与可视视口相交。对于文本节点,交叉 矩形是包含所拥有文本节点的集合中所有文本节点的边框框的最小矩形,并与可视视口相交。

6.8. 发出容器计时条目

当要求为 Document document 发出容器计时条目时,执行以下步骤。应在处理完所有绘制 操作后,每帧调用一次:
  1. 如果 document 不存在待处理的容器计时更改,则返回。

  2. 对于 document容器根记录映射中的每个 containerRootrecord

    1. 如果 recordhasPendingChanges 为 false,则继续。

    2. 给定 recordcontainerRoot创建容器计时条目

    3. recordhasPendingChanges 设置为 false。

    4. recordlastNewPaintedAreaElement 设置为 null。

    5. recordlastNewPaintedAreaSize 设置为 0。

  3. 将该 Document 标记为不再存在待处理的容器计时更改。

注:与其他一些可能为每个已绘制元素发出 多个条目的绘制计时 API 不同,容器计时会为每个容器根累积已绘制区域,并且每个容器根每帧至多发出一个 PerformanceContainerTiming 条目。这种批处理方式效率更高,并可提供容器绘制状态的整体视图。

6.9. 获取父容器根 Element

给定一个 HTMLElement element,要获取 父容器根元素,执行以下步骤:
  1. parentelement 的 parentElement。

  2. 如果 parent 为 null,则返回 null。

  3. 返回给定 parent获取容器根元素的结果。

6.10. 对容器计时产生贡献

如果以下条件全部为真,则一个 HTMLElement容器根的容器计时作出贡献

要确定一个 HTMLElement element 是否为容器根 containerRoot 的容器计时作出贡献:
  1. 如果 element 为 null,则返回 false。

  2. 如果 element 在影子树中,则返回 false。

  3. 如果 element 不是 containerRoot 的后代,则返回 false。

  4. 如果 element忽略的子树内,则返回 false。

  5. 返回 true。

6.11. 获取容器根 Element

给定一个 HTMLElement element,要获取 容器根元素,执行以下步骤:
  1. 如果 element 为 null,则返回 null。

  2. 如果 element 的 [^containertiming^] 内容属性存在,则返回 element

  3. 如果 element 的 parentElement 不为 null,则返回给定 element 的 parentElement 时获取 容器根元素的结果。

  4. 返回 null。

6.12. 可能更新最后一个新绘制区域

要为容器计时记录 record酌情更新最后新增的绘制区域,给定一个 Document document、一个容器根 HTMLElement containerRoot、一个 HTMLElement element、一个 DOMRectReadOnly enclosingRect 和一个绘制计时信息 paintTimingInfo, 执行以下步骤:
  1. paintedRegionrecordpaintedRegion

  2. 如果 paintedRegion 完全包含 enclosingRect,则返回。

  3. newPaintedAreaenclosingRect 中尚未 包含在 paintedRegion 中的面积。

  4. recordpaintedRegion 设置为 paintedRegionenclosingRect 的并集。

  5. recordlastNewPaintedAreaPaintTimingInfo 设置为 paintTimingInfo

  6. 如果 newPaintedArea 大于 recordlastNewPaintedAreaSize

    1. recordlastNewPaintedAreaElement 设置为 element

    2. recordlastNewPaintedAreaSize 设置为 newPaintedArea

  7. recordhasPendingChanges 设置为 true。

  8. 如果 containerRoot 的 [^containertimingignore^] 内容属性存在,则返回。

  9. parentContainerRoot 为以 containerRoot 为参数获取父容器根元素的结果。

  10. 如果 parentContainerRoot 为 null,则返回。

  11. parentRecorddocument容器根 记录映射中对应于 parentContainerRoot 的条目。如果不存在这样的条目,则将 parentRecord 设置为以 paintTimingInfoparentContainerRoot 的 [^containertiming^] 内容属性值为参数创建容器计时记录的结果;然后将 (parentContainerRootparentRecord) 添加到 document容器根记录映射中。

  12. 给定 documentparentContainerRootelementenclosingRectpaintTimingInfo,为 parentRecord 可能更新最后一个新绘制 区域

注:lastPaintedElement 是在单个渲染帧中贡献最大新绘制面积的元素。这可以避免 依赖于特定于实现的绘制顺序。lastPaintedElement 旨在作为调试辅助工具,供开发者调查是什么驱动了大型或复杂容器根(例如 表格)的更新,而不是作为独立的性能指标。发出条目后会重置lastNewPaintedAreaSize, 以便每一帧的比较都重新开始。

注:此算法会报告已绘制 区域的任何变化,而不论其大小。即使已绘制面积仅发生 1 像素的变化,也会导致一个新的 PerformanceContainerTiming 条目被排入队列。希望过滤较小变化的开发者可以通过比较条目之间的 size 值来实现。

6.13. 创建容器计时条目

为了在给定一个容器计时记录 record 和一个容器根 HTMLElement containerRoot 的情况下创建容器计时条目,用户代理必须执行以下步骤:
  1. entry为一个新的PerformanceContainerTiming 条目,并将其:

  2. 将 PerformanceEntry 加入队列entry

7. 安全与隐私考量

7.1. 跨源限制

该 API 遵守跨源边界:

7.2. 信息暴露

此 API 提供的大多数信息都已经可以通过现有 API 进行估算:

该 API 不会暴露:

7.3. 计时攻击

该 API 使用 DOMHighResTimeStamp, 出于安全目的,它可能受到分辨率限制,这与其他 Performance API 一致。

7.4. 隐私考量

该 API 不会:

8. 致谢

非常感谢以下人士提供的宝贵反馈和建议:

一致性

文档 约定

一致性要求通过描述性断言 与 RFC 2119 术语的组合来表达。 在本文档规范性部分中,关键词 “MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、 “MAY” 和 “OPTIONAL” 应按 RFC 2119 中的描述进行解释。 不过,为了可读性, 这些词在本规范中并不全都以大写字母出现。

本规范的所有文本都是规范性的, 明确标记为非规范性的章节、示例和注释除外。[RFC2119]

本规范中的示例会用 “for example” 一词引入, 或者通过 class="example" 与规范性文本分隔开, 如下所示:

这是一个资料性示例的例子。

资料性注释以 “Note” 一词开头, 并通过 class="note" 与规范性文本分隔开, 如下所示:

Note,这是一个资料性注释。

索引

本规范定义的 术语

由引用定义的 术语

参考文献

规范性参考文献

[DOM]
Anne van Kesteren。DOM Standard。现行标准。 URL:https://dom.spec.whatwg.org/
[GEOMETRY-1]
Sebastian Zartner; Yehonatan Daniv。Geometry Interfaces Module Level 1。URL:https://drafts.csswg.org/geometry/
[HR-TIME-3]
Yoav Weiss。High Resolution Time。URL:https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al。HTML Standard。 现行标准。URL:https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola。Infra Standard。现行标准。URL:https://infra.spec.whatwg.org/
[PAINT-TIMING]
Ian Clelland; Noam Rosenthal。Paint Timing。 URL:https://w3c.github.io/paint-timing/
[PERFORMANCE-TIMELINE]
Nicolas Pena Moreno。Performance Timeline。URL:https://w3c.github.io/performance-timeline/
[RFC2119]
S. Bradner。Key words for use in RFCs to Indicate Requirement Levels。1997 年 3 月。最佳当前实践。URL:https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu。Web IDL Standard。现行 标准。URL:https://webidl.spec.whatwg.org/

资料性参考文献

[INTERSECTION-OBSERVER]
Stefan Zager; Emilio Cobos Álvarez; Traian Captan。Intersection Observer。URL:https://w3c.github.io/IntersectionObserver/

IDL 索引

[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;

partial interface HTMLElement {
    [CEReactions, Reflect] attribute DOMString containerTiming;
    [CEReactions, Reflect] attribute boolean containerTimingIgnore;
};