在真实的上网体验里,有一种让人瞬间烦躁的场景:你正要点 立即购买,页面突然往下跳了一截,手指落点变成了 加入收藏;你刚读到一段关键结论,顶部插入一条推荐位,正文整体下移,你只能重新找刚才的段落。CLS(Cumulative Layout Shift)就是用来把这种 页面抖动 量化出来的指标,它不关心你加载有多快,而是关心内容在视觉上是否稳定,是否尊重用户的注意力与操作意图。
下面我会以浏览器内核与渲染链路的视角,把 CLS 的定义、计算方式、工程指导意义、常见成因、调试手段与治理策略讲透,并穿插贴近真实业务的案例,让抽象指标变成可执行的设计与开发准则。
CLS 到底是什么:它衡量的不是 快,而是 稳
CLS 的核心目标是衡量 视觉稳定性。更准确地说,CLS 不是把页面生命周期里所有布局位移简单相加,而是取 最大的一段突发位移(largest burst)。官方定义里,CLS 统计的是页面整个生命周期中所有 非预期(unexpected)布局位移里,得分最高的那段 会话窗口(session window)。该窗口的规则是:位移之间间隔少于 1 秒会被归为同一段突发窗口,并且整个窗口最长只计算到 5 秒。(web.dev)
这个定义非常关键,因为它直接影响你的排查方向:
- 你不需要为用户停留
10分钟后才发生的零星位移背锅(新版定义弱化了长时间停留带来的累积惩罚)。(web.dev) - 你更需要盯住
加载初期、首屏附近、关键交互前后那些会形成突发窗口的位移链条。(web.dev)
浏览器到底在记录什么:Layout Instability API 与 layout-shift 条目
从内核实现角度,浏览器并不是凭感觉判断 抖动,而是基于 Layout Instability API 记录 layout-shift 性能条目:当一个在视口内可见的元素,在两帧之间其 起始位置(start position,和书写模式相关)发生变化,就会被认为发生了布局位移。(web.dev)
这里有两个容易误解的细节:
- 仅仅
新增 DOM或某个元素自身尺寸变化并不必然算布局位移;只有当它导致其它可见元素的起始位置改变,才会产生layout-shift。(web.dev) - 如果位移是用户可预期的,比如点击按钮展开区域,并且位移紧贴用户输入发生,浏览器会用
hadRecentInput标记把它从CLS计算中排除(典型阈值是输入后500ms内)。(web.dev)
从渲染管线看,布局位移几乎总是 布局阶段(layout / reflow)后的副产物:资源到达、样式变化、字体替换、脚本插入节点,都会触发样式计算与布局树更新;一旦布局结果改变并进入下一帧提交(commit),就可能形成可见位移。你可以把 CLS 理解成:浏览器在连续帧里对 可见内容位置稳定性 的审计报告。
CLS 怎么算分:impact fraction × distance fraction,它惩罚的是 影响范围 与 移动距离
每一次 layout-shift 都会产生一个分数,公式是:
layout shift score = impact fraction * distance fraction (web.dev)
impact fraction(影响比例):不稳定元素在前后两帧中可见区域的并集,占视口面积的比例。它反映有多少屏幕内容被牵连。(web.dev)distance fraction(距离比例):不稳定元素移动的最大水平或垂直距离,占视口最大边长(宽或高取其大)的比例。它反映移动有多狠。(web.dev)
举一个非常直观的算分例子:某个首屏大卡片占视口一半高度,加载后整体向下挪动了 25% 的视口高度。影响比例大约 0.75,距离比例 0.25,这次位移得分就是 0.1875。(web.dev)
这套设计的巧妙之处在于:
- 小范围的轻微抖动不会被过度惩罚;
- 大范围推挤、把用户操作目标挪走的位移会迅速拉高分数;
- 同一段
会话窗口内连续发生多次位移,分数会叠加,最终取最大窗口作为CLS。(web.dev)
什么才算 好:阈值、分位数与 字段数据(Field Data)的真实含义
行业里常用的 CLS 分档是:
Good:CLS ≤ 0.1Needs Improvement:0.1 < CLS ≤ 0.25Poor:CLS > 0.25(web.dev)
但更重要的是官方强调的统计口径:要以 75th percentile(第 75 分位)来衡量,并且需要分别按 mobile 与 desktop 分组。(web.dev)
为什么是 75th percentile?因为它更能代表 多数用户里最糟糕的那批体验,而不是平均值的温柔幻觉。PageSpeed Insights 也明确说明它会在字段数据里报告各指标的 75th percentile,用来帮助开发者理解最令人沮丧的用户体验。(Google for Developers)
再把 字段数据 讲得更工程一点:
- 实验室测量(Lab)适合开发期回归测试,但无法覆盖真实用户设备、网络、缓存、第三方脚本时序的差异;
CLS这类强依赖加载时序与资源竞争的指标,特别容易出现本地复现不了,线上用户很痛的情况。CLS指南就点出:开发环境缓存命中、接口本地极快、第三方内容差异,都会导致你在开发时看不出位移,但真实用户会看到。(web.dev)
如果你在 Google Search Console 里看 Core Web Vitals 报告,它强调数据来自真实世界使用数据,并按 URL group 聚合,状态由 CLS / INP / LCP 里最差的那个决定。(Google Help)
CLS 的指导意义:它改变的是设计决策与渲染策略,而不只是 修 bug
很多团队把 CLS 当成上线后性能治理的一项 检查项,这会错过它真正的价值:CLS 是一个把 视觉布局稳定性 变成硬指标的工具,它会反向影响你在组件库、广告策略、字体策略、骨架屏策略、动效实现方式上的选择。
1)CLS 是对 注意力经济 的约束:稳定就是信任
页面每一次非预期位移,都在打断用户的视觉定位。特别在电商、支付、表单这类高意图场景,位移会造成两类成本:
- 误触成本:点击目标被挪走;
- 认知成本:用户要重新扫描页面找回上下文。
这就是为什么 CLS 是 Core Web Vitals 的三大指标之一,与 LCP(加载体验)、INP(交互响应)并列,代表用户体验的不同侧面。(web.dev)
2)CLS 把工程问题精准指向 布局输入的不确定性
在内核视角里,布局是一个函数:
Layout = f( DOM, CSS, Fonts, Images, Viewport, WritingMode, ... )
一旦其中任何输入在首屏阶段是 不确定 的(例如图片尺寸未知、广告高度未知、字体度量未知、异步数据插入未知),布局结果就会在后续帧被改写,位移几乎不可避免。CLS 的治理本质就是:
- 尽量让布局输入在首次布局前就确定;
- 或者即便输入延迟到达,也要用
占位把布局结果锁住。
3)CLS 让你重新审视 动效实现路径:transform 是朋友,top/left/height 常常是敌人
在渲染流水线里,改变 top / left / width / height 往往会触发布局,继而可能引发位移;而使用 transform(合成层动画)可以在不触发布局的前提下完成移动与缩放。CLS 指南明确建议:要移动元素尽量用 transform: translate(),要缩放用 transform: scale(),避免直接改布局属性。(web.dev)
典型成因全解:每一种都对应一类渲染时序问题
Optimize CLS 指南把最常见的元凶列得很直接:
- 没有尺寸的图片
- 没有尺寸的广告、嵌入内容、
iframe - 动态注入内容
- Web fonts (web.dev)
我会把它们映射到更底层的渲染机制,方便你在架构层面预防。
成因 A:图片或视频尺寸未知,导致解码后回填尺寸
真实世界案例(电商商品流)
你做了一个商品列表,卡片结构是:图片在上,标题与价格在下。为了省事,图片用 img { width: 100%; height: auto; },但没有提供 width / height 属性,也没有用 aspect-ratio 锁定占位高度。
在 Wi-Fi 下你看不出问题,因为图片几乎瞬间加载完成;可在弱网或冷缓存下,首屏先完成一次布局:图片高度未知时往往被当成 0 或者一个临时高度,标题与价格先挤到上面;图片到达、解码、得知固有宽高后,布局重算,卡片高度突然撑开,把后续卡片整体往下推,一串位移在 1 秒内连续发生,直接形成 CLS 突发窗口。(web.dev)
解决方式(让布局输入提前确定)
- 给
img写width与height属性,让浏览器在下载前就能计算占位比例; - 或者在容器上用
aspect-ratio锁定占位; - 对于响应式图,仍然可以用
width/height表达固有比例,CSS 再控制实际渲染尺寸。
更像工程规则的一句话是:首屏媒体元素必须可在首次布局时确定占位盒模型。
成因 B:广告位、推荐位、第三方卡片高度不确定
真实世界案例(资讯正文 + 广告插入)
资讯正文加载完成后,你的广告 SDK 异步返回一个 300×250 的素材,但在素材返回前你并没有预留空间。于是页面先按 0 高度排版,用户开始阅读;几百毫秒后广告节点插入正文中间,正文整体下移,用户阅读位置被打断。更糟糕的是,有些广告会二次竞价切换素材尺寸,导致连续两次位移。Optimize CLS 也把 ads / embeds / iframes without dimensions 作为常见原因点名。(web.dev)
解决方式(把不确定性收敛到占位容器里)
- 预留固定高度或最小高度(例如
min-height)给广告槽; - 如果素材尺寸是多档位,预留最大档位高度,内部做居中或裁切;
- 把广告的加载策略从
插入节点变为填充节点:节点先在 DOM 中,尺寸先确定,后续只换内容。
这里的设计哲学很简单:广告可以晚来,但坑位要先挖好。
成因 C:动态注入内容插到视口上方,推挤用户正在看的内容
这类问题在现代前端里很常见:A/B 实验条、登录提示条、toast 顶部横幅、cookie 同意条、智能客服入口。
真实世界案例(登录提示条)
你在页面顶部做了一个登录提示条:未登录时显示,登录后消失。逻辑上它是运行时判断,所以渲染时序经常是:首屏完成布局 → 异步拿到登录态 → 插入顶部提示条 → 整个页面下移。用户的鼠标可能正在对准导航或搜索框,瞬间被挪走。
更优解(用覆盖层替代推挤)
- 对于非关键内容,使用
position: fixed覆盖在顶部,而不是占据文档流高度; - 或者在首屏阶段就预留一条固定高度的区域,登录态回来只是切换可见性而不改变布局。
成因 D:Web fonts 引发 FOUT / FOIT,字体度量变化导致换行与段落高度变化
Optimize CLS 把字体问题讲得很具体:字体加载前可能是 FOUT(先用后备字体显示,后切换)或 FOIT(先不可见,字体到达再显示),两者都可能造成布局位移,因为即便文字不可见,浏览器仍会用后备字体去做布局,一旦 Web font 到达,文字块的度量发生变化,周围内容随之移动。(web.dev)
真实世界案例(品牌字体 + 多语言站点)
你给中文站和英文站统一上了品牌字体。英文段落的后备字体是系统 sans-serif,而品牌字体的字宽略窄。加载前用后备字体排版时每行能放 40 个字符;切换到品牌字体后,每行能放 43 个字符,段落总行数减少,正文高度变小,上方某个图片下移或上移,造成可见位移。用户读到一半会突然 跳行。
解决方式(降低字体切换带来的度量差异)
- 用
font-display: optional,避免因为迟到字体而触发重新布局;(web.dev) - 明确写合适的后备字体,并让后备字体尽量接近目标字体(而不是让浏览器默认回落到差异很大的字体);(web.dev)
- 在支持的环境里用
size-adjust、ascent-override等能力做度量对齐;(web.dev) - 对首屏关键字体用
<link rel=preload>提前加载,提高在首次绘制前到达的概率,从而避免切换引发布局。(web.dev)
调试与定位:把 CLS 从一个分数拆成一帧帧的证据链
CLS 的治理难点不在修复,而在定位:你需要知道 是哪一次位移、谁推了谁、位移发生在什么时序。这就需要把指标分解到浏览器的渲染证据上。
1)用 Chrome DevTools 把位移区域高亮出来
web.dev 提供了一个非常实用的排查技巧:在 DevTools 里开启 Layout Shift Regions,刷新页面后发生位移的区域会被短暂高亮(紫色)。路径是 Settings > More Tools > Rendering > Layout Shift Regions。(web.dev)
这一步特别适合快速回答两个问题:
- 位移发生在首屏还是非首屏?
- 位移集中在顶部横幅、正文插入,还是某个组件内部的自我抖动?
2)用 Performance 面板的 Layout shifts 轨道精确追踪
Chrome DevTools 的 Performance 面板专门提供了 Layout shifts 轨道,位移会以紫色菱形显示,并按时间邻近性聚成 clusters;你悬停可以看到导致位移的元素,点击还能在 Summary 里查看更详细的分数、元素与潜在原因。(Chrome for Developers)
当你把这条轨道与 Network、Main thread、Timings 对齐看时,经常能一眼看穿因果链:
- 某张图片的响应返回 → 解码完成 → 布局重算 → 位移菱形出现
- 某个第三方脚本执行 → 插入节点 → 位移 cluster 出现
- 字体文件完成下载 → 字体切换 → 文本块高度变化 → 位移出现
3)把 CLS 接到真实用户监控(RUM),别只看实验室跑分
Core Web Vitals 强调这些指标应该在 field 中衡量,并建议站点建立自己的真实用户监控。(web.dev)
如果你没有现成的 RUM 服务,web.dev 推荐使用 web-vitals 库:它是一个大约 ~2KB 的模块化库,提供方便的 API 去收集可在字段测量的 Web Vitals,并且确保你采集的数据与 Google 工具的计算方法一致;其中 CLS 的实现是基于 Layout Instability API。(web.dev)
一个非常典型的落地方式是:在前端采集 CLS,用 navigator.sendBeacon 或 fetch 上报到你的日志系统,再按 75th percentile 计算各页面、各业务线的稳定性趋势。web.dev 也提醒:数据采集了但不上报,就等于没做。(web.dev)
顺带提醒一个容易踩坑的差异点:Layout Instability API 默认不会报告 iframe 内部的位移条目,但 CLS 指标本身会把子帧体验算在内,这会导致你在不同数据源(例如 CrUX 与自建 RUM)之间看到差异,需要把子帧位移汇总上报才能对齐。(web.dev)
工程治理清单:把 CLS 变成组件库与上线门禁的一部分
下面这份清单,我更建议你当作 设计与开发规范 来执行,而不是上线后救火。
规则 1:首屏媒体元素必须 可预布局
img必须提供width与height,或容器提供aspect-ratio;video、canvas、iframe同理要预留尺寸;- 骨架屏要和最终内容的盒模型一致,别做
看起来像但尺寸不一致的骨架。
这类规则对应的就是 Optimize CLS 里反复强调的 Images without dimensions、Ads/embeds/iframes without dimensions。(web.dev)
规则 2:动态插入内容要遵守 不推挤阅读流 原则
- 任何运行时才出现的横幅、提示条、推荐位,优先考虑覆盖层(
fixed)或预留坑位; - 如果一定要插入文档流,尽量插在用户视线之外,或用滚动锚定策略避免把用户阅读位置推走。
规则 3:字体策略以 度量稳定 为核心 KPI
把字体当成 布局依赖,不是当成 皮肤资源。可以直接采用 Optimize CLS 给出的组合拳:
font-display: optional- 合理后备字体
size-adjust等度量对齐能力- 首屏关键字体
preload(web.dev)
规则 4:动效只做 合成层友好 的属性变化
当你的动效需要移动或缩放元素:
- 用
transform做translate / scale,避免改top/left/width/height;(web.dev) - 该原则不仅能减少
CLS,也能显著降低主线程布局与绘制压力,让动画更丝滑。
规则 5:把 bfcache 当作降低重复位移的辅助策略
很多站点的位移问题在 返回上一页 时更明显:用户从详情页返回列表页,列表页又经历一遍资源加载与插入,位移再来一次。Optimize CLS 提到:让页面具备 bfcache 资格,浏览器在短时间内可直接恢复离开时的完整状态,从而避免返回时再经历那些常见位移。(web.dev)
这不是让你忽视首屏位移,而是让你在 多页跳转体验 上减少重复伤害。
一个更完整的案例研究:把 CLS 0.32 拉回 0.06 的改造路径(典型内容站)
下面用一个非常贴近现实的案例串起来:某内容站的文章页,字段数据 CLS P75 = 0.32,属于 Poor。目标是进入 Good(≤ 0.1),并且优先修复对阅读与点击影响最大的位移窗口。(web.dev)
现象复盘(用户视角)
- 首屏标题下方正文刚出现,突然插入一条
关注公众号横幅,正文整体下移; - 阅读到第三段时,广告位异步加载,插入正文中间,导致阅读位置丢失;
- 字体从系统字体切换为品牌字体,段落重新换行,出现轻微上移。
这些位移在时间上非常集中,很容易落在同一个 session window,最终被 CLS 取最大突发窗口放大。(web.dev)
定位过程(开发视角)
- 在 Chrome DevTools 开启
Layout Shift Regions,确认位移集中在首屏标题下方与第三段附近;(web.dev) - 在 Performance 面板里查看
Layout shifts轨道,发现三个位移菱形在2秒内形成同一个 cluster,其中广告插入的那次得分最高;(Chrome for Developers) - 用自建 RUM(
web-vitals)采集分布,确认问题主要发生在移动端弱网与冷缓存用户,实验室跑分偏乐观。(web.dev)
改造动作(按收益从高到低)
关注横幅:改为fixed覆盖层,不再占文档流高度;广告坑位:在正文中预先渲染占位容器并固定高度,广告只做内容填充,不再插入新节点推挤;(web.dev)字体:首屏标题字体preload,正文使用font-display: optional,并调整后备字体与度量对齐,减少切换导致的换行差异;(web.dev)
结果与经验沉淀
- 改造后字段数据
CLS P75从0.32降到0.06,进入Good; - 真正决定成败的不是某一条 CSS 技巧,而是把
不确定性从布局阶段移走:要么前置确定,要么用占位收敛。 - 治理完成后,把规则固化进组件库:所有
Banner默认覆盖层模式;所有AdSlot必须带尺寸策略;所有首屏媒体必须声明固有比例;字体加载策略统一封装。
你可以把 CLS 当成一句话的工程准则
如果要用一句话总结 CLS 的指导意义,我会写成:
任何会影响首屏与关键交互区域布局的输入,都必须在首次布局前确定,或被稳定的占位容器所吸收。
这句话背后对应的,就是 CLS 的计算方式、会话窗口逻辑、常见成因列表、字体与动效建议、以及 DevTools 的定位方法。(web.dev)
如果你愿意,我也可以按你熟悉的技术栈(React / Vue / Next.js / Nuxt / 微前端 / Hybrid WebView)给一套更偏代码与架构的 CLS 治理模板:包含组件占位约束、广告与推荐位的插槽协议、字体加载策略、CI 门禁(Lighthouse + 关键页面回归)、以及 RUM 上报与 P75 报表口径。

被折叠的 条评论
为什么被折叠?



