1. 引言
本节为非规范性内容。
网页中 DOM 元素的移动会影响用户体验, 并且在当今网络上非常常见。这种移动通常是由于内容 异步加载并挤占了页面上的其他元素。
布局不稳定性 API 通过为用户会话中的每一帧动画报告一个值 (“布局偏移”)来识别这些不稳定页面。 本规范提供了用户代理计算布局偏移值的方法。
布局偏移值预计与特定时刻布局不稳定的严重程度 大致相关。其计算方法同时考虑了受影响区域的面积及 页面元素被移动的距离。
本规范暴露的这些值不建议用作 “布局变化观察器”,原因有几点。首先,它们与 PerformanceObserver 相关,因此如果用户代理认为影响到 站点性能,则可以延迟分发回调。其次,用户代理可能会忽略 很小的布局偏移。因此,不建议依赖此 API 来运行任何会影响网页用户可见行为的 JavaScript。
1.1. 累积布局偏移(CLS)
本节为非规范性内容。
布局偏移值 代表某一时刻,但如果能有个值代表 用户在页面上停留期间页面的总体不稳定性也很有用。
为此我们提出两个值,用户代理或开发者可以计算 以获得该种表示。(这些定义为非规范性内容, 因为 API 并不暴露这些值。)
-
文档累积布局偏移(DCLS)分数 是在单个 浏览上下文中报告的每个 布局偏移值之和。 (DCLS 分数不包含后代浏览上下文中的布局不稳定。)
-
累积布局偏移(CLS)分数是 在 顶级浏览上下文内报告的每个 布局偏移值 之和,以及在任一 后代浏览上下文 内报告的每个 布局偏移值 的一部分(即 子帧加权因子)。
累计布局偏移分数预计与页面生命周期内 布局不稳定的严重程度大致相关。
开发者可以用这个 API 计算 DCLS 或 CLS 分数, 方法是在这些值报告到观察器后进行累加, 并在 visibilitychange 事件发生时取得“最终”分数。
该策略已在用例示例中说明。
1.2. 来源归因
本节为非规范性内容。
除了布局偏移值,API 还会报告 最多五个 DOM 元素的采样,这些元素的布局偏移 对当前动画帧的布局偏移值贡献最大。 sources 按影响区域从大到小排序,因此第一个元素表示 对布局偏移贡献最大者。
有时,不稳定的真正“根本原因”可能只是 间接与发生布局偏移的 DOM 元素相关。 例如,当新插入的元素挤下了下方的内容时, sources 属性只会报告被移动的元素,而不会报告插入的元素。
我们认为,用户代理要在间接程度上分析 不稳定的成因,实现有意义的“根本原因”归因是不可行的。 但我们认为,这个 API 提供的针对被偏移元素的直接报告, 对于需要诊断布局不稳定现象的开发者,依然大有价值。
1.3. 用例示例
本节为非规范性内容。
let perFrameLayoutShiftData= []; let cumulativeLayoutShiftScore= 0 ; function updateCLS( entries) { for ( const entryof entries) { // Only count layout shifts without recent user input. if ( entry. hadRecentInput) return ; perFrameLayoutShiftData. push({ score: entry. value, timestamp: entry. startTime}); cumulativeLayoutShiftScore+= entry. value; // Sources are sorted by impact area in descending order. // The first element contributed most to the layout shift. if ( entry. sources&& entry. sources. length> 0 ) { console. log( 'Largest contributing element:' , entry. sources[ 0 ]. node); } } } // Observe all layout shift occurrences. const observer= new PerformanceObserver(( list) => { updateCLS( list. getEntries()); }); observer. observe({ type: 'layout-shift' , buffered: true }); // Send final data to an analytics back end once the page is hidden. document. addEventListener( 'visibilitychange' , () => { if ( document. visibilityState=== 'hidden' ) { // Force any pending records to be dispatched. updateCLS( observer. takeRecords()); // Send data to your analytics back end (assumes `sendToAnalytics` is // defined elsewhere). sendToAnalytics({ perFrameLayoutShiftData, cumulativeLayoutShiftScore}); } });
布局偏移分数只是一个信号,只能与用户体验中的“跳跃感” 粗略相关。
建议开发者不必为布局偏移分数的微小变化纠结; 该指标并非为高精度设计,且用户代理 可能会为提高计算效率而降低精度。 此外,该指标的定义未来可能会发生变化。
2. 术语
2.1. 基本概念
起始点:Node
N 在坐标空间 C
中的起始点定义如下:
-
如果 N 是能生成一个或多个
Element,则 N 在 C 中的起始点为:从 C 的原点到其主盒子第一个流方向起始角的二维像素偏移(单位为 像素)。(主盒子的第一个分片) -
如果 N 是文本节点, 则 N 在 C 中的起始点为: 从 C 的原点到 N 生成的第一个 行盒的流方向起始角的二维像素偏移。
与变换无关的起始点:Node
N 在坐标空间 C 的与变换无关的起始点是:
计算 N 在 C 的 起始点,假定任何 发生变换的元素 的 变换矩阵 都为单位矩阵。
注: 判断节点是否发生偏移时, 我们会同时判断有无变换,使节点不会因变换被视为不稳定。 但 CSS 变换始终会被纳入可视表现的计算,以及视口外点的排除。
可视表现:Node
N 的可视表现定义如下:
-
如果 N 是生成一个或多个盒子的
Element,则其可视表现为所有由 N 生成的盒子的所有分片内的点, 使用 视口的坐标空间,排除那些在 视口 之外的点。 -
如果 N 是文本节点, 则其可视表现为 N 生成的所有 行盒 范围内的所有点,坐标空间为 视口, 并排除所有处于 视口 外的点。
若某条件在前一帧内成立(in the previous frame),即它在最近一次 报告布局偏移算法完成后即时成立。
前一帧起始点:Node
N 在坐标空间 C 的前一帧起始点是:
它在前一帧的
N 在 C 的 起始点。
前一帧与变换无关的起始点:Node
N 在坐标空间 C 的前一帧与变换无关的起始点是,
它在前一帧内
N 在 C 的 与变换无关的起始点。
前一帧可视表现:Node
N 的前一帧可视表现为它在前一帧内
的 可视表现。
每个用户代理定义一个显著像素数 (整数),用于计算某次移动是否构成布局偏移。 这样用户代理可以为性能或体验做调整。
若点 A 和点 B 的水平或垂直方向差异达到或超过 显著像素数 个 像素单位,则 A 显著不同于 B。
注: Chrome 将 显著像素数 设为 3。
2.2. 不稳定节点
若Node
N 满足以下条件,则认为其在坐标空间 C 内
发生了偏移:
否则,认为 N 未发生偏移于 C。
若Node
N 满足下列所有条件,则其为不稳定候选节点:
-
N 是以下任一:
-
当前与前一帧 的 computed value 的 visibility 属性均为 "visible";且
-
不存在
ElementP 满足:
注: 有关可滚动溢出区域的条件旨在防止节点仅因滚动操作而被视为不稳定。
若Node
N 是 不稳定候选节点
且不是 行内裁剪穿越者,
则称 N 为不稳定。
Node
N 若满足下列所有条件,则为行内裁剪穿越者:
注: 行内裁剪穿越者示例:一个元素通过行内方向的移动穿过其包含裁剪区域的边界从而进入或离开可视区域。只要这种元素没有在块流方向发生偏移,就不会被算作不稳定节点。这便于特定类型的“轮播”UI 组件实现。
不稳定节点集:Document
D 的不稳定节点集即其所有 不稳定 包含 Shadow DOM 的所有后代 组成的集合。
注: 在首帧时,没有任何节点的前一帧起始点,因此不稳定节点集为空。
2.3. 布局偏移值
移动向量:Node
N 的移动向量是
从
的二维像素单位的偏移量。
移动距离:Node
N 的移动距离是
中的较大值。
最大移动距离:Document
D 的最大移动距离是其 不稳定节点集中
所有 Node
的 移动距离的最大值。
如果 不稳定节点集为空,则为 0。
距离分数:Document
D 的距离分数为以下两者中较小的那个:
节点影响区域:某个
不稳定 Node
N
的节点影响区域是包含下列内容的集合:
影响区域:Document
D 的影响区域为其 不稳定节点集
中所有 Node
的 节点影响区域包含的所有点的集合。
影响分数:Document
D 的影响分数为其 影响区域
的面积除以 视口的面积(或若 视口区域面积为 0,则为 0)。
注: 影响区域面积的计算本质上是二维 Klee 测度问题。一种使用扫描线和线段树、 对 n 个不稳定节点时间复杂度为 O(n log n) 的解法见这里。
布局偏移值:Document
D 的布局偏移值为其 影响分数
乘以 距离分数。
注: 布局偏移值同时考虑了受影响的视口比例和 任一元素实际移动的最大距离。这样就能反映,即使一个很大的元素仅 移动较小的距离,对页面不稳定性的感知影响也较低。
2.4. 输入排除
排除性输入是指 源自输入设备的、表明用户主动与文档交互的事件,或任何 直接改变视口大小的事件。
排除性输入通常包括 mousedown、 keydown、 pointerdown,以及 change events。 但事件仅用于开始或更新轻拂、滚动手势的,不能视为排除性输入。
用户代理可在 pointerdown 事件之后 延迟布局偏移的上报,直到确认该事件未触发轻拂或滚动手势。
mousemove 和 pointermove 事件也不是排除性输入。
3. LayoutShift
接口
[Exposed =Window ]interface :LayoutShift PerformanceEntry {readonly attribute double value ;readonly attribute boolean hadRecentInput ;readonly attribute DOMHighResTimeStamp lastInputTime ;readonly attribute FrozenArray <LayoutShiftAttribution >sources ; [Default ]object (); };toJSON
所有属性的取值均由报告布局偏移步骤赋值。
sources 属性返回一个
FrozenArray
类型、其元素为 LayoutShiftAttribution
对象的数组,按影响区域降序
排列。第一个元素的 节点影响区域最大,代表对布局偏移贡献最大的元素。
实现布局不稳定性 API 的用户代理,必须在
Window
上的 supportedEntryTypes
里包含 ,
以便开发者检测此 API 的支持。
4. LayoutShiftAttribution
接口
[Exposed =Window ]interface {LayoutShiftAttribution readonly attribute Node ?;node readonly attribute DOMRectReadOnly previousRect ;readonly attribute DOMRectReadOnly currentRect ; };
注: previousRect
和 currentRect
属性报告的矩形以 CSS 像素为单位,这与
getBoundingClientRect()、
IntersectionObserver、
ResizeObserver
等其他 Web 平台 API 保持一致。这提供了一套与设备无关的坐标体系,方便将布局偏移测量与其他 DOM 测量关联。
每个 LayoutShiftAttribution
都关联有一个 Node
(即其
关联节点)。
获取 LayoutShiftAttribution
实例
A 的
node 属性时,将
A 的 关联节点 和该节点的
节点 document
作为输入,调用 获取元素(get an element)
算法,并返回其结果。
注: 使用 获取元素算法 可确保若所归因节点已断开连接或在 shadow root 内,node 属性为 null。
获取元素 算法应从 Element Timing 规范中移到更适合本规范复用的位置。
获取元素
算法应泛化为接受
Node
而不仅仅是 Element。
previousRect 和 currentRect 属性的值由创建归因步骤赋值。
5. 处理模型
在 更新渲染步骤(属于 事件循环处理模型)期间, 实现布局不稳定性 API 的用户代理在调用 标记绘制时间 算法后,必须执行如下步骤:
5.1. 报告布局偏移
Document
D 报告布局偏移时,按如下步骤执行:
-
如果 D 当前的 布局偏移值不为 0:
-
用 D 的 关联 realm 创建一个
LayoutShift对象 newEntry。 -
将 newEntry 的
name属性设为。"layout-shift" -
将 newEntry 的
entryType属性设为。"layout-shift" -
将 newEntry 的
duration属性设为 0。 -
将 newEntry 的
value属性设为 D 当前的 布局偏移值。 -
将 newEntry 的
lastInputTime属性设为最近一次 排除性输入 的时间,如果会话期间没有发生过排除性输入,则为 0。 -
将 newEntry 的
hadRecentInput属性设为,如果true lastInputTime少于 500 毫秒前,否则为。false -
将 PerformanceEntry newEntry 对象入队。
-
5.2. 报告布局偏移来源
Document
D 报告布局偏移来源时,按如下步骤执行:
-
对于 D 的 不稳定节点集中的每一个成员 N,执行如下步骤:
-
对 C 按降序排序。若 a 的 节点影响区域面积小于 b 的 节点影响区域面积,则 a 小于 b。
注: 这样可以确保 sources 属性按影响区域降序暴露布局偏移来源,贡献最大者排在首位。
-
对 C 中每个成员调用一次 创建归因算法,返回
FrozenArray类型的LayoutShiftAttribution对象数组。
Node
N 创建归因时,执行如下步骤:
6. 安全性与隐私考虑
布局不稳定性与 资源计时有间接联系, 因为资源加载缓慢可能导致页面出现本不必要的中间布局。资源计时信息可被恶意站点 用于统计指纹识别。布局不稳定性 API 仅报告当前浏览上下文的 不稳定性。它不会直接聚合多个浏览上下文的得分。开发者可手动实现聚合, 但不同源的浏览上下文须协作 才能共享不稳定性得分。