PageSpeed 原本显示 CLS 0.04。开启 Auto Ads,或开发者加了一段”智能”广告注入脚本(扫描 DOM、在段落之间插入广告位)之后,CLS 跳到 0.28,Search Console 把每一篇文章的 Core Web Vitals 都标成”差”。读者的体感非常明确:正读到第三段中间,页面突然下跳 250px,因为一个广告刚刚在他们滚动位置的上方插了进来。这跟经典的”我忘了给 <ins> 加 min-height”完全不是同一个问题——这里的广告位在 LCP 触发时根本不存在于 HTML 里,它们是后来由 JavaScript 创建出来的,每一次出现都是一次布局抖动。
常见原因
按真实案例中出现频率排序。
1. Auto Ads 在首屏渲染后插入正文内广告
Auto Ads 的正文内置广告是在浏览器端扫描 DOM、找出段落分隔点、用 JS 注入 <ins> 标签的方式工作的。哪怕这个广告位预留了高度,“插入一个新块级元素”这个动作本身就会把下方所有内容下推,产生一次等同于广告位高度的布局抖动。
怎么判断:打开页面,等广告加载完,查看渲染后的 HTML。如果 <ins class="adsbygoogle"> 出现在你原始 HTML 里没有的段落之间,就是 Auto Ads 注入的。
2. 自定义广告注入脚本没有骨架占位
很多 CMS 主题自带一段”智能”脚本,遍历正文内容,在每第 N 段后插入广告。如果对应位置没有预渲染的骨架,每一次插入都是一次全新的布局抖动。
怎么判断:在主题代码里搜 insertBefore、insertAdjacentHTML、appendChild,看看附近有没有 adsbygoogle 这种字符串。找到就是它。
3. 响应式广告位选了比预留高度更大的创意
你给广告位设了 min-height: 280px。AdSense 投放了一个 336x600 的创意。广告位向下撑开 320px,正文整体下移。Lighthouse 会把这一次撑开记为 CLS,因为它发生在首次渲染窗口之后。
怎么判断:DevTools → Performance 录一次加载,看 layout shift 条目里有没有 <ins class="adsbygoogle">、且 value > 0.05 的事件。如果对应节点的高度小于实际投放尺寸,就是这条原因。
4. 锚定广告或插页广告盖住内容时改了 body padding
锚定广告(屏幕底部固定)正常情况下不应该造成 CLS,因为它是覆盖在内容之上而不是把内容推下去。但如果实现方式是加载后给 <body> 加一段 padding-bottom,那这一步加 padding 就是一次抖动。
怎么判断:对比广告加载前后 <body> 的样式。如果 padding-bottom 从 0 变成了 90px,那锚定广告的实现是在挤压版心,而不是浮于其上。
5. 同意横幅延迟出现,导致同意后再渲染广告位
CMP 横幅在第 1.0 秒出现,用户在第 2.5 秒点了同意,然后页面才开始注入此前不存在的广告位。每一个同意之后才出现的广告位都计入 CLS——因为它们出现在页面早已可交互之后。
怎么判断:在 DevTools Performance 里时间倒流,看广告位是否只在用户点击同意之后才出现。是就是这条。
6. 懒加载广告恰好在用户滚动时触发
懒加载广告在进入视口时才加载。如果广告位没有预留高度,而用户正好在滚动过程中触发它,抖动就发生在用户视线焦点正中央——Lighthouse 会按高影响力计权,CLS 暴涨。
怎么判断:Search Console → Core Web Vitals → “CLS issue” 抽样 URL。报告里有抖动前后的截图。如果抖动元素是懒加载广告位,就是它。
开始前先确认
- 在 PageSpeed 和 CrUX 里抽样 5 个文章 URL 确认 CLS 回归是真实的,别只信一次 Lighthouse。
- 记录当前 PageSpeed 分数,后面好测增量。
- 确认你用的是 Auto Ads、手动广告位还是两者都用——修复路径不同。
- 记录 CMP 厂商和同意流程的时序。
需要收集的信息
- PageSpeed Insights “Layout Shift Elements” 诊断项——会列出每一个抖动节点。
- 开启 “Web Vitals” 覆盖层的 DevTools Performance 录制。
- 广告位插入点的原始 HTML(view-source)和渲染后 DOM 的 diff。
- AdSense → 广告 → 按网站 → 编辑 里的 Auto Ads 密度设置。
- 任何在
DOMContentLoaded之后还会改动正文 DOM 的自定义 JS。
一步步修复
按影响和成本排序。
第 1 步:每一个广告位置都预渲染骨架占位
把”运行时注入”替换成”首屏 HTML 里就已存在”:
<article>
<p>第一段...</p>
<p>第二段...</p>
<div class="ad-slot" data-position="in-article-1" style="min-height: 280px; width: 100%;">
<ins class="adsbygoogle"
style="display:block; text-align:center;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-XXXX"
data-ad-slot="YYYY"></ins>
</div>
<p>第三段...</p>
</article>
<div> 在首屏渲染时就已经存在,且预留了高度。AdSense 后来填充内容时,DOM 里没有新元素加入,只是它的 <iframe> 内容变了——CLS 接近 0。
第 2 步:如果你已有手动广告位,关掉 Auto Ads 的正文内插入
AdSense → 广告 → 按网站 → 你的域名 → 编辑 → Auto ads:
- 关闭 “In-article ads”
- 关闭 “In-feed ads”
- 只在确认锚定不会挤压版心的前提下保留 “Anchor”
实际操作中 Auto Ads 和手动广告位往往叠加。挑一条策略走到底。如果你想要 Auto Ads 的省心,把密度调到 Low,只靠它的侧边栏 / 锚定位置出广告。
第 3 步:给每个广告位预留足够高度
响应式广告位要按”最大可能投放尺寸”预留,而不是平均:
.ad-slot[data-position="in-article-1"] { min-height: 336px; }
.ad-slot[data-position="sidebar"] { min-height: 600px; }
.ad-slot[data-position="header"] { min-height: 90px; }
是的,如果实际投放的是更小的创意,会有可见的白边。这点白边比一次 CLS 抖动便宜得多。目标是 min-height 等于你 90 分位投放尺寸。
第 4 步:把 CMP 的决策提前,而不是延后
如果 CMP 是页面加载后才注入的,切换成”先阻塞、再异步”的模式:
<head>
<script src="https://your-cmp.example/cmp.js" data-mode="prerender"></script>
<!-- CMP 必须在 adsbygoogle.js 加载前完成回调 -->
</head>
大多数主流 CMP(Google Funding Choices、OneTrust、Cookiebot)都有 “stub 模式”:在 HTML 里预留同意决策的空间,而不靠 DOM 改动来呈现。把它打开。
第 5 步:把锚定广告改成 overlay 定位
如果锚定单元在挤压版心,改它的外层:
.adsense-anchor-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
/* 千万不要给 body 加 padding-bottom */
}
锚定浮于上方。不要给 body 加 padding。如果担心内容被锚定盖住,确保锚定单元本身带关闭(X)按钮——AdSense 默认是有的。
第 6 步:懒加载也要有高占位,不能是 0 高度
在广告位容器上用 loading="lazy" 时,占位也必须预留空间:
<div class="ad-slot lazy" style="min-height: 280px;">
<ins class="adsbygoogle" loading="lazy" ...></ins>
</div>
少了 min-height,懒加载广告位在用户滚到它之前一直是 0 高度——一进入视口就撑开。CLS 灾难现场。
第 7 步:在真机上重新测
PageSpeed 跑的是干净的 Chrome 没扩展的环境。真实用户要么开了广告拦截(抖动反而少),要么有跟踪器(抖动更多)。在以下环境测:
# 用 Lighthouse CLI 跑你的 URL
npx lighthouse https://your-site.example/article-slug --view --preset=desktop
npx lighthouse https://your-site.example/article-slug --view --preset=mobile
桌面和移动都要 CLS < 0.1。
验证
- PageSpeed Insights 上之前被标记的 URL 现在 CLS < 0.1。
- Search Console → Core Web Vitals → “Poor (CLS)” 数量在 28 天内归零。
- 手动在 4G 网速下滚动一篇文章——首屏渲染后没有可见跳动。
- DevTools Performance 中 LCP 时间点之后没有
value > 0.05的 LayoutShift。
长期防止
- 把每一个广告位都当作一等 HTML 元素对待,预留高度,绝对不从 JS 注入。
- 每个页面模板只挑一种广告策略(Auto Ads 或手动),同一模板里不要混用。
- 按 90 分位投放尺寸预留,不要按平均——留白比抖动便宜得多。
- 在 CI 里对样本文章 URL 跑 Lighthouse;CLS 回归超阈值就让构建失败。
- 安装任何新的”智能广告布局”插件前先按本清单审一遍。
- 每月看一次 Search Console 的 Core Web Vitals;CLS 回归通常在 14-28 天后显现。
常见坑
- 给经常投放 250px 创意的正文广告位只预留
min-height: 50px——保证每次访问都有 200px 的抖动。 - 在面板里关了 Auto Ads,却忘了去掉手动广告位上的
data-ad-format="auto"属性——它们还在跑。 - 只在桌面端测试,而 CLS 回归只发生在移动端(不同设备投放尺寸不一样)。
- 只看一篇文章的 Lighthouse 就以为整站没问题——CLS 会因文章长度和广告密度变化。
- 给
<body>加padding-bottom: 90px来给锚定让位,而加 padding 本身就是一次布局抖动。
FAQ
Q:我预留了高度,CLS 还是 0.18,怎么回事?
预留高度大概率没对上实际投放尺寸。在 DevTools 里看广告加载后的真实高度,跟你设的 min-height 比。真实高度更大,就把预留值改大。也可以看一下 AdSense 拖慢页面。
Q:我可以等广告加载完再显示它吗?
不行——那样会造成更大的抖动。正确的解法是预留空间,不是延迟显示。
Q:关掉 Auto Ads 会不会让收入断崖?
带预留空间的手动布局通常比全开 Auto Ads 的 RPM 低 5-15%,但换来的是 CWV 过关。如果你的页面因 CWV 差被搜索降权,流量损失远超 RPM 差距。详见 Auto Ads 位置不合理。
Q:我怀疑就是 CMP 的锅,怎么证明?
在 staging 环境里完全禁用 CMP,重跑 Lighthouse。如果 CLS 掉到 0.05 以下,CMP 就是抖动源。找厂商要预渲染的同意 UI,或者换 CMP。
Q:Search Console 会因为 CLS 高直接降权吗?
是间接的——CWV 是排名因素,“Poor” 状态的页面会被降权。恢复要看 CrUX 现场数据(不是实验室数据)持续 28 天通过。相关的内容质量信号问题见 瘦内容页 AdSense 审核。