1.前言
Cumulative Layout Shift (CLS),翻译过来就是累积布局偏移。它是一个以用户为中心、用来衡量可视区域元素稳定性的重要标准,可以帮助我们定量计算出用户遇到意料之外的布局偏移的频率,CLS小可以确保我们的页面有一个良好的用户体验。
不知道你有没有遇到这样的情况,当你正在网上阅读一篇文章时,眼前的页面突然就出现了一些变化,而在这之前没有任何的警告,你正在阅读的内容就这样不知道去哪了。更糟糕的是,你可能正准备去点击一个链接或者一个按钮,就在你点击的那一刹那,这个链接或按钮的位置变化了,最后你点击了另外的一个地方!大多情况下这种体验都只是让人感觉很不爽,但是有些情况下就不止于此了。
网页中的内容经常出现预料之外的移动多数是由于一些异步加载的资源或者是动态添加DOM元素到当前页面上造成的. 罪魁祸首可能是
-
尺寸未知的图片或视频
-
实际呈现比其原有大小更大或更小的字体
-
可以动态调整自身大小的第三方广告或者页面小组件
-
...
让这个问题变得更加棘手的是,网站的功能在开发环境和实际用户体验中有很大不同。个性化的和第三方的内容在开发环境和线上环境下经常会表现不一致,比如,用来做测试的图片大多数情况下都已经缓存在了开发者的浏览器中了,另外,本地调用的API通常都比较快,以至于在线上环境可能出现的一些延迟被在开发阶段被开发者给忽略了。
CLS可以通过测量真实线上用户所遇到以上问题的频率来帮助你来解决这些问题。
2.什么是CLS
CLS会计算出页面整个生命周期中所有发生的预料之外的布局偏移的得分的总和。
每当一个可视元素位置发生改变,就是发生了布局偏移。
那么,怎样才算是一个好的CLS得分?
对照上图,一个拥有良好用户体验的网站的CLS得分应该尽可能的小于0.1.为了确保对于大多数用户都能达到这个分数,起码应该对移动设备和台式机设备上75%的页面浏览总量进行测量。
3.从细节上看一看图层变换
布局偏移这个概念是在 [Layout Instability API](https://github.com/WICG/layout-instability) 中定义的,每当视口内的可见元素改变了它最初的位置该API就会上报发生布局偏移的属性,比如在默认[书写模式](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode)下它的top和left, 像这样的元素被视为不稳定元素。
需要注意的是,只有在可视元素改变自身的最初位置才表示发生了布局偏移。如果一个新元素被添加至DOM上或者可视元素更改了自身的尺寸大小,都不会作为布局偏移,也就是说发生的变化如果没有引起可视元素更改其最初位置,那么这个变化就不会被当做是一次布局偏移。
4.布局偏移得分
浏览器会根据视口的大小和不稳定元素在两帧之间发生的位移来计算出布局偏移得分。最终得分是该位移的两个度量维度的乘积,分别是影响分数和距离分数。
layout shift score = impact fraction * distance fraction
5.影响分数
影响分数测量的是从上一帧到下一帧不稳定元素对视口的影响范围大小。
视口内所有不稳定元素从上一帧到当前帧在视口内占据的可视区域的总和作为当前帧的影响分数。
从上图可以看到,第一帧元素占据了视口高度的一半,下一帧元素下移了视口高度的25%。红框标识了元素在两帧里总共占据的可视区域,这个值是视口高度的75%,因此影响分数是0.75.
6.距离分数
CLS计算公式中的另一部分是距离分数,它测量的是不稳定元素相对视口移动的距离。 具体计算方式是:
距离分数 = 不稳定元素在垂直或者水平方向上移动的最大距离(以较大者为准) / 视口的最大尺寸(宽度或者高度,以较大者为准)
在上面的例子中,视口的最大尺寸以高度为准,不稳定元素在视口内下移了视口高度的25%,距离分数是0.25。
在这个例子中影响分数是0.75, 距离分数是0.25,那么最终的CLS得分就是 0.75 * 0.25 = 0.1875。
最初,CLS的计算仅基于影响分数来。引入距离分数是为了避免出现大尺寸元素发生小位移而计算出的CLS过大的情况。
下面的例子说明将内容添加到现有元素是如何影响CLS的。
“Click Me”按钮被添加到上方的灰色盒子内的最下方,致使下方的绿色盒子向下移动而且一部分内容已经超出了视口。
灰色盒子的尺寸变大,但是起始位置并没有改变,所以它不能被视为不稳定元素。
“Click Me”按钮原先并不存在DOM中,所以它的起始位置改变与否无从谈起,也不能被视为不稳定元素。
而对于绿色盒子,它的起始位置的的确确发生了变化,但是由于它的部分内容已经移动到了视口之外,而不可见区域是不能参与计算影响分数的,所以两帧之间绿色盒子参与计算影响分数的部分只是上面红色框内的部分,这一部分的大小和第一帧中绿色盒子的大小是一样的,都是视口的50%,因此最终影响分数是0.5。
距离分数是由上面紫色箭头标识的部分计算而来,绿色盒子向下移动了大概视口高度的14%,所以距离分数是0.14.
最终视图的CLS为 0.5 x 0.14 = 0.07。
上面的例子都是对于单个不稳定元素而言,下面就看看多个不稳定元素下是如何计算CLS的
第一帧中,从API返回中拿到了4个动物,他们按照字母表的顺序依次排列。第二帧中,列表中多了其他的动物。
两帧之间“Cat”的起始位置没有发生变化,所以它是稳定元素,相似的,所有新元素都是稳定元素。但是“Dog”, “Horse”, “Zebra”的起始位置都改变了,所以这3个元素都是不稳定元素。
红框区域就是这个三个不稳定元素在两帧之间所占据的可视区域大小的总和。“Dog”的旧位置加上新位置总共是两个li,同理“Horse”、“Zebra”都是如此。理论上他们最终占据的区域应该是6个li,但是需要注意的是“Dog”的新位置其实是占据了“Horse”的旧位置,所以最终要减去这一个li,最终是5个li。
上面3个箭头分别代表了这3个不稳定元素所移动的距离,其中,蓝色箭头代表了”Zebra” 移动的距离,大概是视口高度的30%,是这3个移动距离中的最大值,因此上述例子的距离分数是0.3。
最终的CLS得分是 0.38 x 0.3 = 0.1172.
7.预料之中的布局偏移 vs 预料之外的布局偏移
并不是所有的布局偏移都是负面的,事实上,很多动态的网站应用经常会去修改一些元素的初始位置。
7.1用户主导的布局偏移
只有当出现了用户预料之外的布局偏移才算是负面的,相对的,为了响应用户的交互(点击链接、点击按钮或者在输入框中输入内容)而发生的布局偏移大体上都是正面的。只要布局偏移发生在与交互足够接近的位置那么用户就能清晰的知道这两者之间的关系。
例如,如果一个用户调用了网络请求,这个请求可能要等一会才能完成,好的做法是添加一个loading效果避免使用户对请求结束之后发生的布局偏移感到不愉快。如果用户没有意识到页面正在加载某些东西,或者不知道请求的资源什么时候能够准备好,他就可能在这期间尝试点击其他内容,而这些内容就可能在请求的资源到来之后往下移动。
在用户输入的500毫秒内发生的布局偏移会带有[hadRecentInput](https://wicg.github.io/layout-instability/#dom-layoutshift-hadrecentinput)标志,可以根据这个字段将其从计算中排除。
7.2动画和过渡
动画和过渡如果用的好,就可以做到在用户不感到突兀的情况下更新页面内容。在页面上突然发生意料之外的布局偏移大多数情况下都会产生不友好的用户体验。但是,如果内容是很自然平缓地从一个位置移动到另一个位置就会帮助用户更好的理解当前正在发生的这些变化,并且对用户起到引导作用。
CSS [transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) 属性 允许你给元素设置动画,而不会引起布局偏移,下面是具体的使用方法:
-
使用transfom: scale() 代替宽、高来改变元素尺寸大小
-
移动元素时,避免修改top、right、bottom和left 属性,而是使用 transform: translate()
8.如何测量CLS
CLS可以在开发阶段来测量,也可以在真实线上测量,下面列出了一些可用的工具
注意:开发阶段毕竟不能代替线上环境,开发中我们通常只会去加载页面,从而这些工具给页面生成的报告的CLS值可能小于线上的CLS值。
8.1线上测量工具
-
[Chrome User Experience Report](https://developers.google.com/web/tools/chrome-user-experience-report)
-
[PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/)
-
[Search Console (Core Web Vitals report)](https://support.google.com/webmasters/answer/9205520)
8.2开发测量工具
-
[Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/)
-
[Lighthouse](https://developers.google.com/web/tools/lighthouse/)
-
[WebPageTest](https://webpagetest.org/)
8.3使用JavaScript来测量CLS
测量CLS(以及所有其他重要的web线上指标)的最简单方法是使用web-vitals JavaScript库,该库将我们需要手动测量CLS的所有复杂点都封装到了一个函数中:
import {getCLS} from 'web-vitals'; // Measure and log the current CLS value,
// any time it's ready to be reported.
getCLS(console.log);
要手动测量CLS,可以使用 [Layout Instability API](https://github.com/WICG/layout-instability). 下面的示例演示了如何创建一个PerformanceObserver来侦听各个发生了布局偏移的实例并将它们记录到控制台:
// Use a try/catch instead of feature detecting `layout-shift`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry);
}
});
po.observe({ type: 'layout-shift', buffered: true });
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
CLS是【发生的所有布局偏移】去掉【最近的用户输入发生的布局偏移】之后的总和, 所以要计算CLS,需要声明一个存储当前CLS得分的变量,然后当检测到新的布局偏移时来进行递增。
与其每一次CLS更改时都去上报,不如跟踪当前的CLS值并在页面的生命周期状态变为隐藏状态时去做上报,因为这种CLS的变动可能会非常频繁,而频繁上报可能会带来性能问题。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
.box {
position: absolute;
left: 0;
top: 0;
transition: all 0.2s linear;
width: 300px;
height: 300px;
background: blue;
}
.active {
top: 100px;
}
</style>
</head>
<body>
<div id="box" class="box">我是一个盒子</div>
<script src="./cls.js"></script>
<script>
setTimeout(() => {
document.getElementById('box').classList.add('active');
}, 2000);
</script>
</body>
</html>
js
// Sends the passed data to an analytics endpoint. This code
// uses `/analytics`; you can replace it with your own URL.
function sendToAnalytics(data) {
const body = JSON.stringify(data);
console.log(body);
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
// (navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
// fetch('/analytics', { body, method: 'POST', keepalive: true });
}
// Use a try/catch instead of feature detecting `layout-shift`
// support, since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Store the current layout shift score for the page.
let cls = 0;
function onLayoutShiftEntry(entry) {
console.log(entry);
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
// Create a PerformanceObserver that calls `onLayoutShiftEntry` for each entry.
const po = new PerformanceObserver((entryList, po) => {
entryList.getEntries().forEach((entry) => onLayoutShiftEntry(entry, po));
});
// Observe entries of type `layout-shift`, including buffered entries,
// i.e. entries that occurred before calling `observe()` below.
po.observe({
type: 'layout-shift',
buffered: true,
});
// Log the current CLS score any time the
// page's lifecycle state changes to hidden.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// Include any pending records not yet dispatched,
// and send the CLS value to an analytics endpoint.
po.takeRecords().forEach((entry) => onLayoutShiftEntry(entry, po));
sendToAnalytics({ cls });
}
});
} catch (e) {
console.log(e);
// Do nothing if the browser doesn't support this API.
}
9.如何改善CLS
对于大多数网站来说,可以通过遵循一些原则来避免意料之外的布局偏移:
-
始终在图像和视频元素上设置尺寸相关属性,或者通过[CSS aspect ratio boxes](https://css-tricks.com/aspect-ratio-boxes/)等方式保留所需的空间。这种方法可确保浏览器在加载图像时可以在文档中为其分配正确的空间。另外还可以使用[unsized-media feature policy](https://github.com/w3c/webappsec-feature-policy/blob/master/policies/unsized-media.md) 功能部件策略在支持功能部件策略的浏览器中强制执行此行为。
-
除非响应用户交互,否则切勿在现有内容上方插入内容,这样可以确保发生任何的布局移位都在预期之中。
-
使用过渡动画相比修改top、left这类属性所做的动画更好。对过渡进行动画处理,可以提供状态与状态之间的上下文的连续性。
原文:[https://web.dev/cls/?utm_source=devtools](https://web.dev/cls/?utm_source=devtools)