cover_image

页面CLS 优化实践

黄海荣、边凯 之家技术
2023年11月01日 00:00

关注“之家技术”,获取更多技术干货

图片

图片

总篇228篇 2023年第42篇


1. CLS 诞生背景

在深入探讨优化实践之前,让我们先了解一下 CLS(累计布局偏移)及其优化的重要性。我们在浏览网页时可能会遇到这样的情况:当我们正聚精会神地阅读网页内容时,突然发现内容在没有任何预警的情况下被挤到了另一个位置。尤其是当您已经阅读了一小段文字后,不得不费劲寻找原来的阅读位置,这种意想不到的偏移可能会让您感到极度不悦。更糟糕的是,当您准备点击一个链接或按钮时,正巧在手指按下的瞬间,链接突然移位,导致您误点到其他内容。通常情况下,这样的网站会给用户带来极差的体验,甚至瞬间的位移错误交互可能造成一定程度的破坏。

视频加载失败,请刷新页面再试

刷新


通常,页面元素发生意外偏移的原因包括异步资源加载以及在 DOM 元素上方动态插入新的元素等。具体而言,这些偏移可能源于图片或视频元素尺寸未设置、不受控的第三方广告引入或者小组件自身大小的动态调整。
因此,我们需要一个指标来度量意外偏移对用户“视觉稳定性”产生的影响。


2. CLS 定义

        CLS (Cumulative Layout Shift) 累计布局偏移指标,通过度量“视觉稳定性”反映用户对页面布局偏移所产生的主观视觉体验。

        Google 定义每隔 5 秒作为一个 CLS 监听"时间窗口",并且要求在每个"时间窗口"内,相邻两次偏移的时间间隔小于 1 秒。在单个"时间窗口"内,CLS 值是由多个元素的偏移值累加得到的。如果页面中的元素布局持续发生偏移(持续时间大于 5 秒),则可能会形成多个"时间窗口",每个"时间窗口"的 CLS 值可能不同。在这种情况下,将最大"时间窗口"的 CLS 值作为该页面的最终 CLS 值。

如下图:CLS = Max(session window 1,session window 2,session window 3) = 0.105

图片


2.1

如何对 CLS 评级?

谷歌定义页面 CLS 数值应该控制在 0.1 以内,包括移动和桌面设备。为了确保大多数用户达成目标,一个良好的测量阈值为第 75 个百分位数。

图片

3. CLS 如何计算

3.1

不稳定性元素

任何位于可视区域内的可见元素,如果其起始位置在两帧之间发生变化,则被视为“不稳定元素”。这些“不稳定元素”用于计算布局偏移。需要注意的是,如果新元素添加到 DOM 或现有元素的尺寸发生改变,只要这些变化不导致其他可见元素的起始位置发生变化,它们就不会被计算为布局偏移。

3.2

计算公式

布局偏移分数 = 影响分数(impact fraction) * 距离分数 (distance fraction)

举例 :  0.07    =  0.5 * 0.14

► 影响分数

        该计算因子的含义是测量“不稳定元素”对两帧之间的可视区域产生的影响,即可视元素的起始位置在两帧之间发生变化后,两帧中元素可视区域的合集占总可视区域的百分比。

    如下图所示:

图片

        上图中黄色块 P 标签为“不稳定元素”,其自身元素占可视区域的 50%,在两帧中移动了可视区域高度的 25%,红色虚线表示两帧中元素的可见区域集合,该集合占总可视区域的 75%,因此在本例中影响分数为 0.75。


► 距离分数

        该计算因子的含义是,测量不稳定元素相对于可视区域在一帧中位移的最大距离(该位移距离可以是水平或者垂直方向,同时存在时取最大者),除以可视区域的最大尺寸维度(尺寸维度可以是宽度或高度,取较大者)。如下图所示:

图片

        上图中最大可视区域尺寸维度是高度,不稳定元素位移的距离为可视区域高度的 25%,因此距离分数为 0.25 。

        根据公式:布局偏移分数 = 影响分数 * 距离分数,以上图为例 CLS 值为 0.75(影响分数)* 0.25(距离分数)= 0.1875 。

3.3

举个例子

图片

        如上图所示,点击 “Click Me!” 按钮时绿色框部分为不稳定元素,发生了向下的位移,其影响范围为红色虚线框部分(底部超出可视区域不做计算),占总可视区域的 50%,即影响分数为 0.5 。

        距离分数由紫色箭头表示,在高度尺寸维度发生了位移,其位移距离为可视区域高度的 14%,即距离分数为 0.14 。

        所以布局偏移分数是 0.5 * 0.14 = 0.07 。

3.4

符合预期的布局偏移

布局偏移并不总是坏事。(布局偏移只有在用户并不期望其发生时才算是坏事), 以下两种情况, 不会影响 CLS 分数。

(1)由用户发起的布局偏移

       用户交互(如单击链接、点选按钮、在搜索框中键入信息等), 500毫秒之后发生的 CLS 都会带有"hadRecentInput"标记, 不会影响 CLS 分数 。  

注意 : "hadRecentInput" 仅适用于不连续输入事件 (如 tap, click, or keypress), 而连续输入事件不会被标记(如 scrolls, drags, or pinch and zoom gestures)

        (2)动画和过渡

        动画和过渡如果做得好,确实是一个在更新页面内容时不让用户感到突兀的好方法。CSS transform 属性可以帮助我们在不触发布局偏移的情况下为元素设置动画:

        用 transform: scale() 来替代和调整 height 和 width 属性。

        如需使元素能够四处移动,可以用 transform: translate() 来替代和调整 top、right、bottom 或 left 属性。


4. CLS 测量

        在上部分中对 CLS 的基础理论进行了介绍,并通过相关示例演示了 CLS 值是如何计算的,接下来我们看一下在浏览器中如何通过工具对 CLS 进行跟踪测量。

4.1

DevTools

        打开 Chrome DevTools,在 Performance 标签选项卡中点击“录制”按钮并刷新页面,您将得到 CLS 的跟踪信息,大部分情况下您可以通过该功能还原定位线上 CLS 问题。  

        如下图:

图片

        点击不同的“红色块”可以查看其对应的 CLS 值及 DOM元素和相关偏移信息。如下图:

图片

4.2

通过 JavaScript 测量 CLS

      我们可以使用 JavaScript 和浏览器原生的 PerformanceObserver 来测量 CLS,通过监听 layout-shift 条目,并根据每 5 秒为一个窗口期的最大分数累计规则,对这些监听到的 layout-shift 条目数据进行计算。

          实现代码如下:

let clsValue = 0;let clsEntries = [];let sessionValue = 0;let sessionEntries = [];
new PerformanceObserver((entryList) => {  for (const entry of entryList.getEntries()) {    // 只将不带有最近用户输入标志的布局偏移计算在内。    if (!entry.hadRecentInput) {      const firstSessionEntry = sessionEntries[0];      const lastSessionEntry = sessionEntries[sessionEntries.length - 1];      // 如果条目与上一条目的相隔时间小于 1 秒且      // 与会话中第一个条目的相隔时间小于 5 秒,那么将条目      // 包含在当前会话中。否则,开始一个新会话。      if (sessionValue &&          entry.startTime - lastSessionEntry.startTime < 1000 &&          entry.startTime - firstSessionEntry.startTime < 5000) {        sessionValue += entry.value;        sessionEntries.push(entry);      } else {        sessionValue = entry.value;        sessionEntries = [entry];      }      // 如果当前会话值大于当前 CLS 值,      // 那么更新 CLS 及其相关条目。      if (sessionValue > clsValue) {        clsValue = sessionValue;        clsEntries = sessionEntries;        // 将更新值(及其条目)记录在控制台中。        console.log('CLS:', clsValue, clsEntries)      }    }  }}).observe({type: 'layout-shift', buffered: true});

4.3

web-vitals Javascript 库

        GoogleChrome 提供了 web-vitals JavaScript 开源库,用于测量用户浏览器端真实性能,除 CLS 指标外还包括 FID(First Input Delay)、TTFB(Time to First Byte)、FCP(First Contentful Paint)、LCP(Largest Contentful Paint)、INP(Interaction to next Paint)。代码示例:

import { onCLS } from 'web-vitals';
onCLS((metric) => {  console.log(metric);});

5. CLS 采集与上报

        通过使用 web-vitals JavaScript 库,我们可以便捷地获取页面的 CLS 和其他性能指标,这在很大程度上简化了数据测量工作。接下来,为了收集用户页面的真实 CLS 性能数据,我们可以设计和开发一套数据采集 sdk,将采集到的数据上报至服务器端进行存储。随后,通过后端大数据分析展示页面的整体 CLS 性能水平,从而为开发人员提供有针对性的优化建议。

5.1

采集、上报时机

        在数据采集和上报过程中,我们应确保数据的准确性和完整性,同时也要充分考虑对采集上报时机的设置,这样可以确保对整体数据统计评分的公平性。

        CLS 的"时间窗口"为 5 秒,因此在 sdk 初始化后的第 5 秒,我们将页面性能数据进行数据采集与上报。然而,sdk 作为一个独立的 JS 文件,可以在页面中通过同步或异步加载。特别是在异步加载方式下,如果延迟 5 秒后再进行采集上报,很可能已超过第一个 CLS "时间窗口"。这样的数据采集可能会影响最终统计评分的公平性,其影响主要体现在以下两个方面:

► 大于 5 秒上报

        受用户停留页面的时间因素影响,时间越靠后用户数据丢失的概率就越大。

图片

         如上图所示,采集工具 js(红框内) 下载时机在第 3.3 秒开始下载,如果在此基础上再延迟 5 秒后采集上报数据,真实的采集时间为 8.3 秒,假如用户停留时长小于 8.3 秒,此情况下若不采取措施该采样数据将会遗漏掉。

► 小于 5 秒上报

        相反,如果将延时时间设定为小于 5 秒(如参考 Sentry@7.64.0 的默认设定时间 1 秒),所得的测量值准确性将得不到保证。为此我们用 web-vitals 做了一次数据分析对比实验,在日均 pv 约 10 万左右的网站上持续 3 天对 CLS 数据进行测量,并分别设定在 5 秒和 1 秒后采集和上报。对比结果发现 1 秒上报其各项指标数据丢失率非常大,且数据值偏小,无法保证其准确性。

        如下图所示:

图片

► 最优方案

        为了解决 sdk 初始化时间大于或小于第一个 CLS "时间窗口"所产生的影响,我们通过研究 web-vitals 源码发现 CLS 以及其他性能指标以 PerformanceNavigationTiming 的 "startTime" 为相对起点。因此,可以将 sdk 初始化时间与该 "startTime" 相减。如果两者的差值大于 5 秒,则可以立即进行数据采集与上报;反之,可以将距离 5 秒的差值作为延迟等待时间。

     PerformanceNavigationTiming 模型如下图所示:

图片

► 页面关闭时采集上报

        当用户停留时长不足 5 秒提前关闭页面时,可能导致数据采集丢失。为了解决这个问题,我们可以通过监听 visibilitychange、pageHide 事件来采集页面关闭前的 CLS 数据。这种方案尽可能地确保数据不丢失,但无法保证第一个"时间窗口"的 5 秒准确度。

      因此,该方案所采集上报的数据只是尽可能的反映页面真实采样 PV,并不参与最终的统计评分。

5.2

采集流程设计

        因为 CLS 值的获取是一个持续进行的过程,在监听 DOM 布局偏移过程中,数据对象通过异步回调方式传递。这个持续动作可能会贯穿整个页面生命周期。由于获取到的数据需要上报到服务器端,不断的进行数据上报将对后端服务带来巨大压力。因此,需要设计一个合理的采集流程以避免上述问题。

        采集流程如下图所示:

图片

   1) sdk 初始化时已满足 5 秒,则立即执行 CLS 数据采集并上报,反之延迟至 5 秒再完成采集上报。

  2)用户停留时长不足 5 秒时页面关闭,监听 visibilitychange、pageHide 事件完成采集,并通过 sendBeacon 方式上报。

   3) 整个页面生命周期中只允许一次采集、上报。

5.3

数据上报

        在上面流程设计图中我们设计了两种上报方式,sdk 初始化后 5 秒上报采用 XmlHttpRequest,页面隐藏或关闭采用 navigator.sendBeacon。

        两种方式详细对比见下图:

图片

5.4

CLS 元素辅助定位

        在发现网站页面的 CLS 值较高时,我们需要使用开发的 sdk 协助开发人员将 CLS 值与其 DOM 元素关联起来,以便确定页面中累计偏移较大的元素。Web-vitals v3.0.0 及更高版本中新增了 attribution 调试功能,能够将 DOM 元素与 CLS 值关联并输出。然而,这导致了包体积增加了 7K,并且输出内容格式固定,无法实现定制化。

        为了定位导致 CLS 值过大的元素,同时防止 js 库体积变大,我们对 web-vitals 中 onCLS 返回的 layout-shift 条目对象进行自定义解析,并对 DOM 元素的 “domPath”路径输出,既帮助开发人员快速定位、修复问题,又缩小了 js 库体积。

        如图:

图片

6. 优化实践

        利用 sdk 我们已收集取到了页面真实数据,下面通过一个真实案例分享我们是如何进行 CLS 优化的。

6.1

页面结构介绍

        为了便于大家对页面结构有清晰的了解,我们在优化之前对页面结构按功能做了如下划分。

1)  文章内容区(Vue 渲染)。

2)  广告部分(之家广告 js 加载)。

3)  页头页尾(公共 js 加载)。

4)  其它(各类第三方 js 库,如统计、埋点、前端各类组件等)。

如图所示:

图片

6.2

页面逻辑说明

        本项目采用 SSR + Vue 实现前端页面渲染,在功能职责上 SSR 仅提供一个空页面模版,其主要内容的渲染工作由 Vue 承担,广告位的渲染部分被包含其中,广告位渲染链路为:

第一步,渲染文章内容部分,由 Vue 绑定数据异步实现。

第二步,判断有无广告位,先由 ajax 异步请求 isHave 接口,判断是否存在广告位,如存在则动态下载广告位 js 及相关资源(图片、样式文件)。

第三步,渲染广告位, 广告 js 及资源文件下载完成后,动态插入 DOM 并完成渲染。

6.3

页面问题说明

        Vue 渲染在广告位 js 及相关资源的加载之后,且广告位资源的下载需要等待 isHave 异步请求返回后,导致广告内容的渲染过程链路被拉长。其次广告位内容尺寸未知,因此页面在渲染过程中页面会因广告位的动态加载渲染出现一次“抖动”现象,最终导致页面的 CLS 值过大。

下图动画演示了广告出现的整个渲染链路:

图片


6.4

优化措施及内容

在开始优化之前我们需要理顺各个资源的功能及依赖关系,并将资源根据功能做了如下划分:
► 本部门公用资源
公共 js 库。
统计代码库。
► 其它资源库及页面基础静态资源
页头、页尾资源文件。
广告库。
页面静态资源(css、js)。
优化思路为:让广告位提前并独立加载渲染,在 SSR 服务端渲染时保留广告位, 并在 SSR 渲染完文章部分后,第一时间由客户端发起请求,获取广告相关信息。
相关优化措施如下:
1)  渐进式加载,优先保证首屏广告位提前渲染,渲染顺序为:文章内容(SSR)-> 广告(DOM传送门)-> Vue 应用 -> 页头页尾。
2)  广告渲染从 Vue 应用中剥离,并在 SSR 渲染时动态预留广告位 “DOM 传送门” 用以稳定 CLS,Vue 与广告位两者独立渲染,两者再无关联。
3)  广告位“DOM传送门”区域预设固定尺寸,当 dom 解析到某个广告坑位时,通过 js 将之前提前渲染好的广告移动到当前广告坑位。
优化前后效果对比,如下图:
图片

6.5

数据评分效果


图片


7. 总结

通过本文我们对 CLS 进行了详细的阐述,并介绍了如何对 CLS 数值进行追踪获取,最后通过真实案例分享如何对 CLS 进行优化来提升用户体验。CLS的优化对网站性能和用户体验的提升是很重要的,除此之外它还包括以下几个方面的影响提升:
►  搜索引擎优化
Google已经将 CLS 作为其评估网站性能的核心指标之一,因此优化CLS值对于提高搜索引擎排名非常重要,该指标的提升会吸引更多的流量。
► 转化率
页面元素的稳定性直接影响到用户在网站上采取行动的可能性。如果页面布局不稳定,用户可能会在尝试填写表单、点击按钮或完成购买等操作时遇到困难。因此,一个稳定的页面有助于用户完成目标操作,从而提升相关业务转化率。
总之,CLS指标是衡量网站性能和用户体验的重要标准,优化CLS值可以提升用户体验、搜索引擎排名、转化率等,为网站带来长期收益。


[参考引文] 

https://web.dev/optimize-cls/

作者简介

图片


黄海荣

 经销商技术部-数据架构团队

 2019年加入经销商数据架构团队,参与前端工程化相关的工作,全程参与了 ftwo 前端监控系统,并推广至全公司前端应用。


图片


边凯

 经销商技术部-数据架构团队

2022年加入经销商数据架构团队,主要负责前端性能优化, 架构和工程化相关工作。


图片


阅读更多:


▼ 关注「之家技术」,获取更多技术干货 

图片


继续滑动看下一个
之家技术
向上滑动看下一个