本文约 3,985 字,预计阅读 10 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:正文读到一半,图片加载完把文字顶走了
手机上读一篇长文,滚动到第三屏时图片才开始请求,加载完成瞬间整段文字向下跳了一截,手指正好点错位置。这个现象就是 CLS(Cumulative Layout Shift,累计布局偏移)。Google 把 CLS 列为三大核心网页指标之一,良好阈值是 0.1,超过 0.25 属于「差」,不仅体验糟糕,也会间接影响搜索排名。
CLS 的头号元凶几乎总是图片:缺少 width 和 height 属性时,浏览器在图片下载完成前不知道该给它留多高的盒子,只能先按 0 高度渲染,等图片到达再撑开,于是下方内容整体位移。WordPress 从 5.5 起已经默认给内容里的图片加 loading="lazy" 并尝试补全尺寸,但主题自定义的图片输出、the_post_thumbnail() 的某些用法、以及手工粘贴进来的 <img> 标签,仍然会漏掉这些属性。本文从机制讲到补齐代码。图片尺寸问题常常和 主题广告位与 CLS 布局偏移控制 一起出现,两者要通盘处理。
原理分析:CLS 是怎么算出来的,宽高属性为什么能救场
CLS = 影响分数(impact fraction)× 距离分数(distance fraction)。影响分数是发生位移的元素占视口的比例,距离分数是它移动的距离占视口的比例。一次 0.05 的小位移看似无害,滚动过程中累积十次就能突破 0.1。
浏览器有个机制叫「宽高比推断」:只要 <img> 带了 width 和 height 属性,即使图片还没下载,浏览器也能用这两个数字算出宽高比,配合 CSS 的 height: auto 提前预留出正确高度的盒子。图片到达后直接填满,不产生任何位移。这就是「加两个属性就能解决 CLS」的原理。
WordPress 5.5 引入的 wp_filter_content_tags() 会遍历 the_content 里的 <img>,自动补 loading="lazy"、decoding="async" 以及缺失的尺寸。但它有几个边界:只能处理保存到数据库的内容里的标签,主题模板里硬编码的 <img> 不走这个流程;wp_get_attachment_image() 输出的属性可以被 wp_get_attachment_image_attributes 过滤;阈值方面,5.9 起提供 wp_omit_loading_attr_threshold 过滤器,默认豁免第一张图不做懒加载。
| 属性/机制 | 作用 | 引入版本 |
|---|---|---|
loading="lazy" |
延迟加载视口外图片 | WP 5.5 默认 |
wp_omit_loading_attr_threshold |
前 N 张图不做懒加载 | WP 5.9 |
wp_lazy_loading_enabled |
按标签与上下文开关懒加载 | WP 5.5 |
wp_img_tag_add_width_and_height_attr |
控制是否补全尺寸属性 | WP 5.5 |
fetchpriority="high" |
提升首图加载优先级 | 浏览器特性,非 WP 版本 |
保姆级实操
- 先量现状:装 web-vitals 或用 PageSpeed Insights 跑一次,记录当前 CLS 数值与「位移最大的元素」。
- 检查主题输出的 img:打开文章页源码,逐个看
<img>是否带width/height;没有的多半来自主题模板硬编码或旧内容。 - 统一补齐属性:挂
wp_get_attachment_image_attributes,从附件元数据里取尺寸,同时补decoding="async"。 - 首图豁免懒加载:用
wp_omit_loading_attr_threshold把阈值调到 2,并给首图加fetchpriority="high"。 - 处理历史内容:对老文章批量跑一次正则补尺寸,或用
the_content过滤器实时兜底。 - CSS 兜底:给内容区图片加
height: auto,并给容器设aspect-ratio。
<?php
// 子主题 functions.php:统一补齐 width / height / decoding
add_filter( 'wp_get_attachment_image_attributes', 'mimangfei_ensure_image_dims', 10, 3 );
function mimangfei_ensure_image_dims( $attr, $attachment, $size ) {
if ( empty( $attr['width'] ) || empty( $attr['height'] ) ) {
$meta = wp_get_attachment_metadata( $attachment->ID );
if ( ! empty( $meta['width'] ) && ! empty( $meta['height'] ) ) {
$attr['width'] = (int) $meta['width'];
$attr['height'] = (int) $meta['height'];
}
}
$attr['decoding'] = 'async';
return $attr;
}
<?php
// 前两张图不做懒加载,首图提升优先级(避免 LCP 被延后)
add_filter( 'wp_omit_loading_attr_threshold', function( $threshold ) {
return 2;
} );
add_filter( 'wp_get_attachment_image_attributes', function( $attr, $attachment, $size ) {
if ( 'mimangfei-hero' === $size ) {
$attr['loading'] = 'eager';
$attr['fetchpriority'] = 'high';
}
return $attr;
}, 20, 3 );
/* 内容区图片:保留宽高比,配合 width/height 属性预留空间 */
.entry-content img {
max-width: 100%;
height: auto;
display: block;
}
/* 容器级兜底:即便图片属性缺失,也按 16:9 预留 */
.entry-content figure.wp-block-image {
aspect-ratio: 16 / 9;
overflow: hidden;
}
常见踩坑
- 只加 loading 不加宽高:懒加载解决的是带宽,解决不了 CLS。两个属性必须同时存在才有效果。
- CSS 里写死
height: 300px:覆盖了height: auto,宽高比推断失效,图片会被拉伸变形。用aspect-ratio代替固定高度。 - 首图被懒加载拖慢 LCP:默认只豁免第一张,但很多主题的「相关文章缩略图」排在内容前,实际首图成了第二张。要么调阈值,要么对 LCP 图单独加
loading="eager"。 - 案例:某站 CLS 报 0.32,排查发现文章里所有图片都来自 Markdown 转 HTML 的历史内容,
<img>只有 src 没有尺寸。批量跑了一次wp_filter_content_tags()的等价逻辑(对旧内容重新保存一次即可触发),CLS 降到 0.04。教训是:早期导入的内容不会自动享受新版本的过滤能力,需要重新保存或直接改库。 - srcset 里尺寸不匹配:
sizes属性写得过宽,浏览器选了大图,加载慢且占位计算错误。按实际断点写sizes,例如(max-width: 768px) 100vw, 720px。 - 延迟加载与 JS 轮播冲突:轮播图的非首帧图片如果被懒加载,切换时会空白。轮播容器内的图片要显式关掉懒加载,这部分可结合
主题轮播(Slider)组件的性能代价一起处理。 attachment_url_to_postid()在 CDN 域名下取不到 ID:图片走 CDN 后 URL 变了,按 URL 反查附件会失败。补齐尺寸时优先走附件 ID,不要靠 URL 反查。
总结
判断标准很简单:页面上每一个 <img>,都应该同时具备 width、height,并且 CSS 里有 height: auto。满足这条,图片类 CLS 归零;首图再单独加 loading="eager" 和 fetchpriority="high" 保 LCP。行动清单:跑一次 PageSpeed 拿基线 → 挂 wp_get_attachment_image_attributes 补属性 → 调 wp_omit_loading_attr_threshold → 历史内容重新保存或批量修正 → CSS 加 aspect-ratio 兜底 → 复测 CLS。做完这套,绝大多数内容站的 CLS 都能压到 0.05 以内。
