广告位布局偏移:预留高度把CLS压下去

一、一面横幅,能让整页往下跳多远

很多站长的移动端 Core Web Vitals 报告里,CLS(累积布局偏移)长期挂着一个不好看的数字,明明 CSS 已经给所有图片都写了固定宽高,广告位也预留了占位,可报告还是说布局在跳。真正的原因往往不在自己写的 CSS 里,而在一个几乎没人注意的位置:正文最上方或最下方那条广告位,在脚本加载完成、广告返回之后才把容器撑开,把下方所有内容一起往下推。

这条广告位通常来自联盟平台自动插入的代码,或者站里手写的一段 <ins class="adsbygoogle">。它的行为是:先渲染一个高度为零或默认高度的容器,浏览器完成首屏绘制;几十到几百毫秒后脚本执行、广告素材返回,容器高度从 0 变成三百多,整个页面重排一次。用户在首屏读到一半时页面整体下移,手指点到的位置就变了;对爬虫和性能评分来说,这就是一次巨大的布局偏移。

这篇只讲一件事:怎么在不动广告内容、不影响收入的前提下,让这条广告位占住该占的高度,把 CLS 压下去。方法只有三条——预留固定高度、按设备分档、把首屏广告和正文广告分开处理。

二、先量:CLS 到底是谁贡献的

优化之前必须先定位。Chrome 开发者工具的性能面板里有一条 Experience 轨道,点开具体的 Layout Shift 事件,右侧会列出这次偏移涉及的节点,其中 div#aswift_1_host、ins.adsbygoogle、iframe 这类名字基本就锁定是广告。另一种更快的方法是打开 Lighthouse 生成的报告,在「避免巨大的布局偏移」这一项里,它会直接列出偏移元素的路径和贡献比例。

线上环境想长期观察,可以用一个极小的脚本上报,靠 PerformanceObserver 监听 layout-shift 事件,记录每次偏移的 value 和最近的那个目标元素:

new PerformanceObserver((list) => {
  for (const e of list.getEntries()) {
    if (e.hadRecentInput) continue;
    const src = e.sources && e.sources[0];
    console.log(e.value.toFixed(4), src ? src.node.tagName + '.' + src.node.className : 'unknown');
  }
}).observe({ type: 'layout-shift', buffered: true });

hadRecentInput 为真的偏移是用户操作引起的(点开折叠面板之类),不计入 CLS,所以要跳过。看日志里出现频率最高、value 最大的那个来源,就是主攻方向。通常一篇文章如果只有一条顶部广告,它的单次偏移贡献可能在 0.1 到 0.3 之间——而 CLS 的「良好」阈值是 0.1,一条广告就能把整页判成不合格。

三、核心手段:给容器一个确定的高度

布局偏移的本质是「元素原有的尺寸发生了变化」。所以解决的唯一办法是:让容器在广告加载之前就占住最终的高度。CSS 上的写法有几种,各有适用场景。

最直接的是固定高度。在广告位外层加一个容器,高度按广告格式的实际尺寸写死:

.ad-slot-inline {
  display: block;
  width: 100%;
  min-height: 280px;
  overflow: hidden;
  contain: layout paint;
}
@media (max-width: 720px) {
  .ad-slot-inline { min-height: 250px; }
}

min-height 比 height 更安全:广告实际高度若是 280 就正好,若素材返回 300 也只是往下多占一点,不会因为高度被裁掉而显示不全。overflow: hidden 防止广告素材超出容器把别的元素盖住。contain: layout paint 是这里的一个加分项,它告诉浏览器「这个容器的内部变化不影响外部布局」,配合固定高度能把偏移压到 0。

第二种是 aspect-ratio,适合宽度自适应、高度按比例定的位(比如某些展示广告、视频广告):

.ad-slot-ratio {
  width: 100%;
  aspect-ratio: 300 / 250;
  contain: layout paint;
}

现代浏览器对 aspect-ratio 的支持已经很完整,但要注意:它只解决「比例确定」的场景。如果广告实际返回的宽高比和声明的不一致,容器仍然会撑开,只是撑得少一些。所以能用固定 min-height 的地方还是用固定高度,它更可预期。

四、按设备分档:别用一个高度应付所有屏幕

同一个广告位在手机上和桌面上的实际高度差别很大。桌面端横向的响应式广告可能是 90 高、250 高或 280 高,手机端往往是 250 或 300,而很多联盟还会对手机端返回更高的竖版素材。用一个 250px 的高度统一处理,手机端就会一直往下撑,桌面端又会留出一片空白。

分档的原则是按断点写三套,然后取「实测最高的那一个」作为每档的高度。不要凭感觉填,做法是:把页面在 375、768、1440 三个宽度下各刷十次,用上面那段 PerformanceObserver 记录每次广告容器的 getBoundingClientRect().height,取最大值再往上留 10 到 20 像素的余量。

@media (max-width: 480px)  { .ad-slot-inline { min-height: 320px; } }
@media (max-width: 1024px) { .ad-slot-inline { min-height: 280px; } }
@media (min-width: 1025px) { .ad-slot-inline { min-height: 250px; } }

分档只是让「预留」更接近真实值,不是要精确到像素。只要预留值和真实值的差距控制在 20~30 像素以内,单次偏移的 value 就会从 0.2 掉到 0.01 以下,肉眼看不出跳动,评分也不会被拖累。

五、首屏广告与正文广告要分开处理

广告位在页面的不同位置,处理策略完全不同,这是最容易被一刀切搞砸的地方。

正文内的广告位(in-article,插在第二段和第三段之间这种)必须预留高度,但不必担心空白:因为用户本来就还没滚到那里,等滚到时广告基本已经填充完了,几乎不会产生可感知的偏移。这类位置的关键是让「预留高度 = 实际高度」,留下的空白只在广告未填充时可见。

首屏或页面最顶部的横幅要谨慎得多。预留三百像素的高度意味着首屏可见内容被挤走一大块,LCP 的候选元素可能被推下去,用户进来先看到一片空白。对这类位置,更合理的做法是默认不预留,改成在广告填充前后各做一次状态切换:容器初始高度 0,脚本检测到广告已渲染后给容器加上高度。虽然这样仍有偏移,但至少空白时间只有几十毫秒;更好的做法是把首屏广告换成一个不占流的格式(固定定位的悬浮位),或者干脆不放首屏广告。

页面最下方的广告位最省心。它在首屏之外,用户滚到底时广告早就填好了,多数情况下不贡献 CLS。这里唯一要防的是「广告位把 footer 往下推」造成滚动位置跳变,预留一个 min-height 就够了。

六、别让预留变成「永久空白」

预留高度的副作用是:广告没填充(无库存、被广告拦截插件挡掉、AdSense 因内容策略不投放)时,那一块就是一片空白。用户看到的是正文中间突然断开两百多像素,体验比轻微的偏移更差,而且这段时间页面看上去像加载失败。

解决办法是给容器加一个「未填充」的兜底样式,靠属性选择器识别状态。AdSense 在填充失败后会给 <ins> 加上 data-ad-status="unfilled",可以据此把高度收掉:

ins.adsbygoogle[data-ad-status="unfilled"] {
  display: none !important;
}
.ad-slot-empty {
  min-height: 0 !important;
}

配合一小段 JS 在页面加载完成后做判断:如果容器里的 iframe 不存在或高度为 0,就给它加上 ad-slot-empty 类,把预留高度收掉。注意这个动作本身也会产生一次布局偏移——但它是「回收空白」,方向是往上收,用户的阅读位置不会被打断,影响远小于广告加载时的下推。

要提醒的是:广告拦截插件是在本地把脚本拦掉,容器里的 ins 永远不会有填充完成的事件。这种访客占相当比例,所以兜底逻辑必须覆盖「超时未填充」这一种情况——设一个三秒的定时器,到点还没填充就收掉高度,别一直等。

七、容易被忽略的两个连带问题

第一,懒加载与广告的关系。 很多人为了省流量给正文图片加了 loading="lazy",于是图片在滚到视口时才加载、才占位,这本身就是 CLS 来源。图片必须写死 width 和 height 属性(或在 CSS 里定 aspect-ratio),这样浏览器能在图片加载前就算出占位。广告位和图片叠加时,两者都要预留,只修一个仍然会跳。

第二,字体加载导致的文字跳动。 自定义字体加载完成后,文字的行高和字形宽度变化会让整段文本重排,这也会算进 CLS。font-display: swap 能保证文字先显示,但会带来一次「字体切换」的偏移。更稳的写法是给字体加 size-adjust 和 ascent-override 让回退字体的度量接近目标字体,或者干脆正文用系统字体栈。这一条和广告无关,但经常和广告偏移混在一起,导致「明明预留了高度还是不合格」。

八、上线与验证

改完不要只在自己的浏览器里看一次。验证要分三层:

  1. 本地:Chrome 性能面板录制一次加载,看 Experience 轨道里还有没有大的 Layout Shift,点击具体事件确认偏移来源已经不是广告容器。
  2. 线上抽样:用 Search Console 的 Core Web Vitals 报告看移动端的 CLS 分位数。这个数据有延迟,一般要等两到四周才有足够样本,别改完第二天就下结论。
  3. 真实用户数据:如果站上装了 CrUX 相关的上报(或用第三方统计的性能采集),看 p75 的 CLS 是否从 0.2 以上降到 0.1 以内。用 p75 而不是平均值,是因为平均值会被大量「没看到广告」的会话拉低,掩盖问题。

改动本身要能回滚。最稳的方式是把预留高度的规则写在主题的自定义 CSS 里,而不是改模板文件——出问题时在后台把那段 CSS 删掉即可恢复,不用碰任何 PHP。改完记得清缓存,CDN 和站点自身的缓存都要清,否则你会对着旧页面反复怀疑自己改错了。

九、小结

广告位造成的 CLS,本质是「容器尺寸在首屏绘制之后才确定」。解决方案不是禁止广告,而是提前告诉浏览器它要占多大地方:正文内广告按设备分档写死 min-height 并加 contain、首屏广告尽量不放或改成悬浮格式、页面底部广告预留即可。同时补上「未填充时收掉空白」的兜底,避免预留变成一块死白。做完这一套,移动端 CLS 从 0.2 降到 0.05 以内是很常见的结果,而收入端完全不受影响——因为你只是把广告的容器位置提前占住了,素材本身照常加载。

Last modification:November 4th, 2026 at 09:31 pm

Leave a Comment