CLS 总是修不好?因为你只盯着分数,从没拆开看过它

CLS 总是修不好?因为你只盯着分数,从没拆开看过它

开场:一个你大概率经历过的场景

打开 Lighthouse,CLS 0.25,超标了。你做了所有人都会做的事:给 <img>width/height。刷新------0.22。再调调字体------0.19。折腾一下午,还是没到 0.1 的及格线。

你不是不努力。是方向错了:你一直盯着那个总分,从没拆开过它。

那 0.25 不是随便打的。它是若干次具体的跳动 累加出来的,每一次跳动都有一个具体的元凶元素。你不去揪出这些元凶,而是"全局加点尺寸碰运气"------当然经常修不对。

这篇文章把 CLS 从"一个让人头大的分数"彻底拆开:那个分怎么算出来的、怎么揪出每一次跳动的元凶、为什么你以为修了却没生效、怎么用数字证明真修好了。 读完,你修 CLS 就不再是玄学。


一、那个 0.25,先拆开:CLS 分数到底怎么算的

很多人对 CLS 的理解停在"页面跳了,分数就高"。但"跳"怎么变成 0.25 这个数?这背后有一套精确算法,理解了它,你才知道该去哪找问题。

单次跳动:波及多大 × 挪多远

页面每发生一次跳动,浏览器算一个"偏移分数":

scss 复制代码
单次偏移分数 = 影响比例(impact fraction) × 距离比例(distance fraction)

距离比例好理解:所有跳动的元素里,挪得最远的那个,移动距离 ÷ 屏幕的最大边(宽和高取大的)。挪 100px,屏幕高 500px → 0.2。

影响比例 有个反直觉的关键点:它不是"动的元素本身有多大",而是"元素跳动前 + 跳动后两个位置合起来的区域(并集),占屏幕多少"。

为什么用并集?想象一个手电筒光斑从屏幕顶端扫到底端------光斑本身不大,但它扫过的整条路径都亮过、都晃过眼。元素移动也一样:它从 A 到 B,中间那一片区域的用户都看到了东西在闪。所以用"起点+终点的并集"来衡量真实波及范围,比"元素大小"诚实得多。

小例子:一张图加载,把下面 100px 高的内容往下挤了 100px。波及区域(图的位置 + 文字被挤到的区域)约占屏幕 30%,移动距离 100/500=20% → 这次偏移分 = 0.3 × 0.2 = 0.06

多次跳动:不是全加,是"取最惨的那一波"

CLS 里的 C(Cumulative)容易骗人------以为是把页面一辈子的跳动全加起来。绝不是。 那样长页面会无限大,没意义。

真实算法用会话窗口(session window),三条规则:

  1. 两次跳动间隔不到 1 秒,算同一波(同一 session)。
  2. 一波最长 5 秒,到点强制结束,开新一波。
  3. CLS = 所有波里,分数最高的那一波的总分。

注意第三条------取最大的一波,不是全部相加

打个比方:拳击比赛打 12 回合,裁判判输赢不是把两边 12 回合的得分全加,而是看每一回合谁赢 。CLS 同理,它关心的是"你最难受的那一两秒里,跳得有多凶 "------因为人对最差的体验印象最深。分散的小跳(每次间隔很久)不算严重;连环跳(一秒内图片、广告、字体接连撑开)才是真凶,会被累加成高分。

小例子:4 次跳动,A/B/C 在 1 秒内连环发生(分别 0.05/0.08/0.04),D 在 3 秒后单独跳(0.10)。A/B/C 算一波 = 0.17,D 单独一波 = 0.10。CLS = max(0.17, 0.10) = 0.17。看,即便 D 那次单跳更猛,连环跳的 A/B/C 加起来更糟------这就是用户真正难受的时刻。

一个不算的例外:用户主动操作的反馈

用户点了按钮、或输入文字后 500ms 内 的跳动,不计入 CLS。道理很简单:那是用户预期 的反馈(你点"展开菜单",菜单展开推开下面,正常)。算法用 hadRecentInput 标记跳过它们。CLS 只惩罚"用户没招惹它、它自己乱动"。

到这里你已经比 90% 的人懂 CLS 了:它不是玄学,是"波及并集 × 移动距离,按最惨一波算"。但知道分数怎么来,只是第一步------更要命的问题在下面。


二、为什么你总修不对:你丢了最关键的 sources

回到开头的场景:CLS 0.25,你给图片加了尺寸,没降。为什么?

因为你只看了那个 0.25 的总分,从没看过 sources

每次 layout-shift 事件,浏览器不只给你一个分数,它还附赠一份案发现场照片 ------sources 字段。每个 source 里是:

  • node:这次是谁跳了(那张图?那段文字?那个广告位?)
  • previousRect / currentRect:跳之前 / 跳之后的位置和尺寸(从哪挪到哪、变了多大)
  • previousAttrs :跳之前的属性(比如那张图跳之前,到底有没有 width/height)

这就是根因的命脉。 CLS 的根因,业界早就归纳成几类了:

根因 sources 里的铁证
图片/媒体没尺寸 source 是 <img>,previousAttrs没有 width/height,且时间点正好有图片资源加载完
字体加载(FOUT) source 是文字元素,字体资源刚加载完,文字宽高变了
动态插入内容 跳动元素的上方,突然插入了新节点(广告/弹窗/延迟模块)
样式晚到 样式资源刚加载完,多个元素一起重排

所以正确的修法不是"全局加尺寸碰运气",而是:打开 sources,看每一次跳动是谁、为什么,对号入座。 如果你的 0.25 主要是"上方广告位突然插入"贡献的,你给图片加一万次尺寸也没用------根因在广告位的预留空间。

这就是大多数人修 CLS 失败的根本原因 :不看证据,凭经验猜。猜对了算运气,猜错了白忙。sources 是浏览器白给你的证据,不看太亏了。


三、就算找对了根因,你还可能修不对:渲染时序陷阱

假设你看 sources 找对了------就是图片没尺寸。你写段脚本:页面加载后,遍历 img,给没尺寸的补上 width/height。刷新------CLS 还是高。

为什么?这是 CLS 最大的坑:CLS 是在页面加载过程中、布局阶段产生的。你的脚本在页面加载完之后才跑------那时候跳动早发生完了,你补尺寸是亡羊补牢。

打个装修的比方:毛坯房阶段砸墙改户型很容易;等精装修都做完了(瓷砖贴了、家具进了),你再想改结构,就得砸瓷砖、搬家具,代价巨大。CLS 就是发生在"毛坯→精装"的布局阶段 ,你要在那个阶段之前或之中动手,不能等装完了再改。

所以正确的干预时机是渲染之前(技术上叫 document_start,页面脚本还没跑、元素还没画出来的时刻),注入一段样式:

css 复制代码
img:not([width]):not([height]) { aspect-ratio: 16 / 9; }

这样所有没尺寸的图,在第一次画出来之前就占好了位置 ,加载完也不会撑开挤别人。字体、广告位同理:字体font-display: optional广告位 预留 min-height,都是抢在跳动发生前把坑占好。

记住这个反直觉的点:CLS 的修复,时机比方法更重要。 方法对(补尺寸)、时机错(加载后),白搭。


四、修完了,怎么知道真修好了:反事实验证

CLS 降了。但怎么确认是真的修好了,不是这次恰好网络快、广告没加载?

靠"刷新看看感觉"是不行的------感觉会被网络、设备、心理作用骗。要靠对照实验 ,医学上叫反事实:"如果把这个根因去掉,问题还在不在?"

具体做法(它有个朴素的名字,叫配对对照):

  1. 基线 :干预前,把页面加载 6 次 ,每次记下 CLS。比如 [0.19, 0.20, 0.18, 0.21, 0.19, 0.20]
  2. 干预:渲染前补上图片尺寸。
  3. 再加载 6 次 ,记下 CLS。比如 [0, 0.01, 0, 0, 0.02, 0]
  4. 统计检验 :前后各 6 次,看这个差异是不是真的、稳定的,而不是偶然波动。

为什么必须多次 + 统计?因为单次测量充满噪声。就像测降压药效果,你不会只量一次血压就下结论------要吃药前量几次、吃药后量几次,排除那些偶然起伏,才能确认"这药真有效"。CLS 也一样:多次加载、前后配对、统计检验(比如 t 检验看显著性),才能说"修对了,不是运气"。

结果:干预前 0.19 → 干预后 0,统计显著(p<0.05)。铁证:就是这张没尺寸的图导致的,而且我真修好了。 这一步,是"我觉得不跳了"和"数字证明不跳了"的鸿沟。


五、串起来:CLS 从分数到治愈的完整链路

把上面四步连起来,就是一套完整方法:

ruby 复制代码
① 监控时留证据        ② 用证据定位根因       ③ 渲染前干预          ④ 反事实验证
   采 CLS 分数           看 sources:           抢在布局阶段前          基线 N 次
 + 每次 sources          谁跳了?为什么?       补尺寸/预留/字体        干预 N 次
   (案发现场)           (无尺寸图?字体?       (时机比方法重要)        统计证明降
                         注入?对号入座)                                (不是感觉)

四步缺一不可:没监控 不知道有问题;没 sources 找不到真根因(只能猜);没抢渲染时机 修了也不生效;没反事实验证不知道真修好。


这套方法,不只治 CLS

你会发现这套套路很眼熟------找到嫌疑 → 去掉/修掉 → 看症状 → 统计证明,这就是排查任何问题的科学方法:

  • 慢脚本:屏蔽它,看页面还卡不卡(去掉嫌疑看症状)。
  • 内存泄漏:清掉泄漏对象,看内存还涨不涨。
  • 报错:改对代码,看还报不报错。
  • CLS:补上尺寸,看还跳不跳。

CLS 只是它最直观的样子------因为跳动能被肉眼看到、分数能被精确算出、根因(无尺寸图)容易理解。

真正的水平,不在"知道要加 img 尺寸"这种常识,而在:会拆开分数(知道哪波跳动贡献的)、会用 sources 取证(不猜)、懂渲染时序(抢对时机)、会统计验证(证明而非感觉)。 这四点凑齐,你修 CLS 就从"碰运气"变成"可复现的工程"。

而且------这套方法从头到尾不需要 AI:监控靠浏览器原生 API,根因靠证据对号入座,干预靠确定的 CSS/属性,验证靠统计。全是确定、可复现的。可靠,恰恰来自这份确定性。


一句话带走

CLS 高,别急着加尺寸。先拆开那个分数(算法:波及并集 × 距离,取最惨一波)→ 看 sources 取证(谁跳了、为什么)→ 抢在渲染前干预(时机比方法重要)→ 多次加载 + 统计证明降(不是感觉)。 四步都不靠猜,CLS 就从玄学变成可解的工程题。

相关推荐
倾颜1 小时前
会 Vue / React,上手 Electron 真没那么难:前端开发者需要补齐的核心知识
前端
kisshyshy1 小时前
从多页面到SPA:React Router 路由进阶完全指南
前端·javascript·react.js
今日无bug1 小时前
JS 数据类型 + 内存分配:从 8 种类型到栈堆模型
javascript·数据结构
JakeJiang1 小时前
抓到接口还不够:用 AIProxy 改返回、Mock 数据、切测试环境
前端·后端
倾颜1 小时前
从 Web 到桌面:AI Mind Electron Desktop Host 的安全边界设计
前端
Csvn1 小时前
📡 前端错误监控从零搭建:window.onerror 与 unhandledrejection 的完整实践
前端
jarvisuni1 小时前
翻车了!GPT5.6接手Opus4.8的项目之后!
前端·人工智能·ai编程
xiaominlaopodaren1 小时前
three.js地图数学基础(三):实战墨卡托
javascript·gis·three.js
雪碧聊技术1 小时前
力扣 回溯法 | LCR 020. 回文子串
javascript·算法·leetcode