CLS 常见原因与修复
布局偏移是最容易被忽视、也最容易一次修好的指标。它几乎总是「尺寸不确定」造成的。
一、会话窗口内怎么累加
一次布局位移的得分由两部分相乘:
- 影响比例 —— 发生位移的元素占了视口多大面积;
- 移动距离比例 —— 它移动的距离占视口高度的多少。
关键在于它不是取单次最大值,而是在一个「会话窗口」内累加:窗口内连续发生的位移会累计计算。所以「来回抖几次」比「稳稳地跳一次」分数差得多。(会话窗口的具体规则:窗口内两次位移间隔不超过 1 秒就合并进同一窗口,单个窗口最长 5 秒;最终 CLS 取所有窗口里得分最高的那个。)
这直接决定了修复思路:不是让最后一次对齐,而是全程别动。只修最后一次跳动,往往只拿到一半的改善。
阈值方面,「良好」是 ≤ 0.1(按真实用户数据的第 75 百分位判定,详见《Core Web Vitals 与阈值》)。
二、五类最常见的位移来源
① 媒体元素没有尺寸。 图片、视频、iframe 没有 width/height,浏览器在它加载完成前不知道该留多大地方,于是先按 0 渲染,加载完再撑开——下面的内容整体下移。这是占比最高的一类。
② 动态插入内容。 异步拿到的推荐位、广告、公告条在首屏渲染完成之后插入,插在已有内容上方时,下面全部被推走。
③ 字体切换。 自定义字体加载完成后替换 fallback,字宽行高都变了,整段文字重排。
④ 内容晚到。 骨架屏与实际内容的尺寸不一致,替换时发生跳动。
⑤ 触发布局的动画。 用 width、top、height 做动画会持续触发布局;用户没有主动触发的位移同样计入。
三、从占位到字体度量
给媒体预留空间。 最便宜也最有效的一条:
<!-- 用 width/height,浏览器会据此算出宽高比 -->
<img src="hero.webp" width="1200" height="675" alt="" />
/* 或者用 aspect-ratio 直接锁比例 */
.hero-img { aspect-ratio: 16 / 9; width: 100%; }
动态内容要么往下插,要么先占位。 如果必须插在上方,就给它一个最小高度的容器,让它在数据到达前后占据同样的空间:
.promo-slot { min-height: 240px; } /* 内容到达前后高度一致 */
字体切换要对齐度量。 用 size-adjust 让 fallback 字体的度量尽量贴近自定义字体,可以显著减少切换时的跳动:
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 97%;
ascent-override: 92%;
descent-override: 24%;
}
另外 font-display: optional 或 swap 的选择也会影响:前者几乎不产生切换位移,代价是首次可能不生效自定义字体。
动画只动 transform 与 opacity。 这两个属性走合成层,不触发布局,因此不计入 CLS:
/* 会计入 CLS:改的是布局属性 */
@keyframes bad { from { top: 0 } to { top: 16px } }
/* 不计入:只改 transform */
@keyframes good { from { transform: translateY(0) } to { transform: translateY(16px) } }
浮层用覆盖而不是插入。 Cookie 提示、促销条应当覆盖在内容之上(position: fixed 或 overlay),而不是插进文档流把内容推下去。
四、哪些位移不算数
CLS 统计的是整个页面生命周期,不只首屏。滚动之后加载的内容、展开面板引起的位移同样计入——只测首屏会漏掉一大半。
实验室数据常常看不到真实用户的 CLS。 很多位移由用户交互触发(展开筛选、加载更多、切换 tab),实验室跑一遍静态加载是复现不出来的。所以要看真实用户上报的分位数。
通过用户交互后 500ms 内发生的位移不计入(这是有意为之,用户点了按钮内容展开不算问题)。但如果你在点击后延迟很久才插入内容,超过了这个窗口,就会被算进去。
五、跳一次未必比抖三次好
CLS 不看首屏,它统计整个页面生命周期,滚动之后才加载的内容同样计入。
动画也不一定推高 CLS——只动 transform / opacity 的合成层动画不计入,改布局属性的才会。
实验室达标更不能说明没事:真实用户的交互会触发实验室复现不了的位移,要看真实数据。
分数是在会话窗口内累加的,所以修好最后一次跳动远远不够——全程别动才是目标。
CLS 考的不是「最终稳不稳」,而是「从头到尾动没动」——预留空间是最便宜的解法。
CLS 的计算方式见 MDN:CLS,先搞懂「会话窗口」才知道为什么改了一处分数没动。