容器计时 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. 简介

Container Timing API 可用于监测 DOM 中已标注的部分何时显示在屏幕上并 完成其初始绘制。开发者可以使用 containerTiming 属性标记 DOM 的子部分(类似于 Element Timing 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. 术语

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

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

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

duration 属性必须返回 0。

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

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

intersectionRect 属性必须返回 this 的 intersectionRect 值。

size 属性必须返回 this 的 size 值。

firstRenderTime 属性必须返回 this 的 firstRenderTime 值。

lastPaintedElement 属性必须返回 this 的 lastPaintedElement 值。

rootElement 属性 必须返回 this 的 rootElement 值。

当调用 toJSON() 时,运行默认 toJSON 步骤。

注: 用户代理需要维护容器根记录映射,以使被移除的内容不会 引入内存泄漏。具体而言,它可以将条目的生命周期与指向这些 HTMLElement 的弱指针相关联, 以便在这些 HTMLElement 被移除后的某个时间清理它们。由于该映射不会暴露给 Web 开发者,因此这不会暴露垃圾回收的时机。

6. 处理模型

注: 实现容器计时 API 的用户代理 需要在 supportedEntryTypes 中为 Window 上下文包含 "container"。 这允许开发者检测对容器计时的支持。

6.1. 每文档状态

对于每个 Document, 用户代理必须维护一个 容器根记录映射,它将 容器根 HTMLElement 映射到 容器计时记录对象。

每个 Window 都有一个 已分派滚动事件,这是一个初始 设置为 false 的布尔值。

每个 Window 都有一个 已分派输入事件,这是一个 初始设置为 false 的布尔值。

6.2. 对 DOM 规范的修改

一旦 [DOM] 规范完成修改,本节将被移除。

我们按如下方式修改 事件分派算法。

紧接在步骤 1 之后,我们添加以下步骤:

  1. 如果 target 的 相关全局对象是一个 Window 对象,并且 event 的 isTrusted 为 true:

    1. 如果 event 的 type 为 scroll,则将 target 的 相关全局对象的 已分派滚动事件设置为 true。

    2. 如果 event 的 type 是 keydown、mousedown、pointerdown 或 click 之一,则将 target 的 相关全局对象的 已分派输入事件设置为 true。

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 和一个 DOMString identifier 的情况下创建一个 容器计时记录,执行以下步骤:
  1. 令 record 为一个新的 容器计时记录。

  2. 将 record 的 paintTimingInfo 设置为 paintTimingInfo。

  3. 将 record 的 identifier 设置为 identifier。

  4. 返回 record。

6.5. 注册容器根

当一个带有 HTMLElement 的 containerTiming 内容属性连接到文档时:

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

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

  3. 用户代理必须跟踪该 容器根子树内的所有绘制操作,但排除任何 忽略的子树。

6.6. 移除 containertiming 属性

当从一个 containerTiming 内容属性的 HTMLElement element 中移除该属性时,执行以下步骤:
  1. 令 document 为 element 的 节点文档。

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

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

6.7. 断开容器根

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

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

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

6.8. 处理用于容器计时的元素绘制

当一个元素被绘制且用户代理需要处理容器计时更新时,给定一个 Document document、一个 绘制计时信息 paintTimingInfo、一个 HTMLElement element 和一个 DOMRectReadOnly intersectionRect,执行以下步骤:
  1. 令 window 为 document 的 相关全局对象。

  2. 如果 window 的 已分派滚动事件或 已分派输入事件中的任一个为 true, 则返回。

  3. 如果 element 不会计入容器计时,则返回。

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

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

  6. 令 record 为 document 的 容器根 记录映射中对应于 containerRoot 的条目。如果不存在这样的条目,则将 record 设置为给定 paintTimingInfo 和 containerRoot 的 containerTiming 内容属性值后,创建容器计时记录的结果; 然后将(containerRoot → record)添加到 document 的 容器根记录映射中。

  7. 令 enclosingRect 为包含 intersectionRect 的最小外接矩形。

  8. 对于 record,给定 document、containerRoot、 element、enclosingRect 和 paintTimingInfo,可能更新最后新增绘制 区域。

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

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

注:容器计时旨在测量内容的 初始渲染。一旦用户滚动页面或与文档交互,就不再记录 绘制,因为这些绘制很可能是滚动或交互的结果(例如,内容 被滚动到视图中或通过点击显示),而不是页面加载的结果。当前 帧中已经待处理的条目仍会被发出。

6.9. 发出容器计时条目

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

  2. 对于 document 的 容器根记录映射中的每个 containerRoot → record:

    1. 如果 record 的 hasPendingChanges 为 false,则继续。

    2. 给定 record 和 containerRoot,创建一个容器计时条目。

    3. 将 record 的 hasPendingChanges 设置为 false。

    4. 将 record 的 lastNewPaintedAreaElement 设置为 null。

    5. 将 record 的 lastNewPaintedAreaSize 设置为 0。

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

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

6.10. 获取父容器根 Element

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

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

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

6.11. 计入容器计时

如果以下所有条件均为 true,则一个 HTMLElement 会计入一个 容器根的容器计时:

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

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

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

  4. 如果 element 位于一个 忽略的子树中,则返回 false。

  5. 返回 true。

6.12. 获取容器根 Element

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

  2. 如果 element 的 containerTiming 内容属性存在,则返回 element。

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

  4. 返回 null。

6.13. 可能更新最后新增绘制区域

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

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

  3. 令 newPaintedArea 为 enclosingRect 中尚未 包含在 paintedRegion 中的区域面积。

  4. 将 record 的 paintedRegion 设置为 paintedRegion 与 enclosingRect 的并集。

  5. 将 record 的 lastNewPaintedAreaPaintTimingInfo 设置为 paintTimingInfo。

  6. 如果 newPaintedArea 大于 record 的 lastNewPaintedAreaSize:

    1. 将 record 的 lastNewPaintedAreaElement 设置为 element。

    2. 将 record 的 lastNewPaintedAreaSize 设置为 newPaintedArea。

  7. 将 record 的 hasPendingChanges 设置为 true。

  8. 如果 containerRoot 的 containerTimingIgnore 内容属性存在,则返回。

  9. 令 parentContainerRoot 为给定 containerRoot 后获取父容器根元素 的结果。

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

  11. 令 parentRecord 为 document 的 容器根 记录映射中对应于 parentContainerRoot 的条目。如果不存在这样的条目,则将 parentRecord 设置为给定 paintTimingInfo 和 parentContainerRoot 的 containerTiming 内容属性值后,创建容器计时记录的结果; 然后将(parentContainerRoot → parentRecord)添加到 document 的 容器根记录映射中。

  12. 对于 parentRecord,给定 document、 parentContainerRoot、element、enclosingRect 和 paintTimingInfo,可能更新最后新增绘制 区域。

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

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

6.14. 创建容器计时条目

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

  2. 将 entry PerformanceEntry 排入队列。

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;

    [Default] object toJSON();
};

PerformanceContainerTiming includes PaintTimingMixin;

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