图片这条链路,出错通常不是某个开关没开,而是顺序错了:先开了懒加载再回头调尺寸,或者先转成 WebP、后来又加了一个新尺寸,结果一部分图有 WebP、一部分没有,srcset 里还挂着一堆已经不存在的文件。
下面按执行顺序走一遍,每一步给出能直接用的代码和验证方法。
顺序 | 做什么 | 这一步的产物 | 提前或推后做的后果 |
|---|---|---|---|
1 | 确定要生成哪些尺寸 | 尺寸注册表 | 事后再加尺寸,全站要重生成 |
2 | 限制原图上限、设压缩质量 | 生成参数 | 已生成的缩略图仍按旧参数 |
3 | 重新生成缩略图 | 完整的文件集合 | 后面每一步都基于残缺的文件集合 |
4 | 生成 WebP 派生文件 |
| 先转 WebP 再加尺寸,新尺寸没有 WebP |
5 | 校正 | 正确的候选图与渲染宽度 |
|
6 | 懒加载与首图例外 | 加载策略 | 首图被懒加载,首屏先白一块 |
7 | 验证 | 可复核的证据 | 前面全对,也可能被缓存挡住 |
一句话记法:先决定生成什么,再决定怎么生成,最后决定怎么发出去。
做法:打开你自己的页面,用开发者工具量三类宽度——正文内容区最大宽度、列表卡片宽度、缩略图宽度。按这三类注册,不要按「小中大特大」拍脑袋加。
代码放进子主题的 functions.php 或一个自建小插件:
<?php
/**
* 只注册真正用到的尺寸。
*/
add_action( 'after_setup_theme', function () {
add_image_size( 'hero-1600', 1600, 0, false ); // 首屏大图,不限高
add_image_size( 'card-800', 800, 0, false ); // 列表卡片
add_image_size( 'thumb-400', 400, 400, true ); // 正方形缩略图,裁剪
} );
/**
* 让这几个尺寸出现在编辑器的尺寸下拉里。
*/
add_filter( 'image_size_names_choose', function ( $sizes ) {
return array_merge( $sizes, [
'hero-1600' => '首屏大图 1600',
'card-800' => '卡片 800',
'thumb-400' => '缩略图 400',
] );
} );关掉用不到的默认尺寸,减少每次上传生成的文件数量:
add_filter( 'intermediate_image_sizes_advanced', function ( $sizes, $metadata ) {
unset( $sizes['medium_large'], $sizes['1536x1536'], $sizes['2048x2048'] );
return $sizes;
}, 10, 2 );两个坑:
medium_large 这类尺寸名,否则老文章会取不到图。必须放在重生成之前——重生成用的是当时的参数,参数改了就得再跑一遍。
// 超过这个边长先等比缩小再存,避免把相机原图直接塞进 uploads。
add_filter( 'big_image_size_threshold', function () {
return 2000;
} );
// JPEG 质量单独设,其他格式沿用编辑器默认。
add_filter( 'wp_editor_set_quality', function ( $quality, $mime_type ) {
return 'image/jpeg' === $mime_type ? 82 : $quality;
}, 10, 2 );质量值按你的图片内容调:产品图、带文字的截图调低会看出糊,实景照片 80 上下通常看不出来。改完自己放大到 100% 对比一眼再定。
先备份数据库和 wp-content/uploads,再执行:
# 只补缺失的尺寸,速度快
wp media regenerate --only-missing --yes
# 改了质量参数就要全量重生成,量大时按年月分批
wp media regenerate --yes $(wp post list --post_type=attachment --year=2026 --field=ID)先确认主机或 CDN 有没有现成的转换能力,有就别在站内再做一份:
curl -sI -H 'Accept: image/webp' https://example.com/wp-content/uploads/2026/10/photo.jpg | grep -i '^content-type'返回 image/webp 说明链路已经在转换。返回 image/jpeg 才需要自己生成「兄弟文件」,再让服务器按 Accept 协商:
/**
* 为每个已生成的子尺寸同时产出一份 .webp 兄弟文件,文件名形如 photo.jpg.webp。
* 需要 Imagick 或 GD 带 WebP 支持。
*/
add_filter( 'wp_generate_attachment_metadata', function ( $metadata, $attachment_id ) {
if ( empty( $metadata['file'] ) ) {
return $metadata;
}
if ( ! in_array( get_post_mime_type( $attachment_id ), [ 'image/jpeg', 'image/png' ], true ) ) {
return $metadata;
}
if ( ! function_exists( 'imagewebp' ) ) {
return $metadata;
}
$uploads = wp_get_upload_dir();
$base_dir = trailingslashit( $uploads['basedir'] );
$targets = [ $metadata['file'] ];
foreach ( $metadata['sizes'] ?? [] as $size ) {
if ( ! empty( $size['file'] ) ) {
$targets[] = dirname( $metadata['file'] ) . '/' . $size['file'];
}
}
foreach ( array_unique( $targets ) as $relative ) {
$src = $base_dir . $relative;
$dst = $src . '.webp';
if ( ! file_exists( $src ) || file_exists( $dst ) ) {
continue;
}
$editor = wp_get_image_editor( $src );
if ( is_wp_error( $editor ) ) {
continue;
}
$editor->set_quality( 82 );
$saved = $editor->save( $dst, 'image/webp' );
if ( is_wp_error( $saved ) && file_exists( $dst ) ) {
@unlink( $dst ); // 清掉写了一半的文件
}
}
return $metadata;
}, 20, 2 );服务器侧(map 必须放在 http 块里):
map $http_accept $webp_suffix {
default '';
'~*image/webp' '.webp';
}
location ~* ^/wp-content/uploads/.+\.(jpe?g|png)$ {
add_header Vary Accept;
try_files $uri$webp_suffix $uri =404;
}三个注意点:
photo.jpg.webp 这种拼接形式,try_files 才能直接拼出来。Vary: Accept,否则 CDN 和反向代理会把 WebP 响应发给不支持的浏览器。Accept 区分,不然第一个访问者决定了后面所有人拿到什么。srcset 和 sizessrcset 由系统按已生成的尺寸自动输出,一般不用管。sizes 是告诉浏览器「这张图在页面上实际占多宽」,默认值往往和你的布局对不上,浏览器就会挑一张过大的图下载。
add_filter( 'wp_calculate_image_sizes', function ( $sizes, $size, $image_src, $image_meta, $attachment_id ) {
$width = is_array( $size ) ? (int) $size[0] : 0;
if ( $width >= 1200 ) {
return '(max-width: 900px) 100vw, 900px';
}
if ( $width >= 600 ) {
return '(max-width: 900px) 50vw, 400px';
}
return $sizes;
}, 10, 5 );验证方法:把窗口拖到手机宽度,在开发者工具 Network 里看这张图实际下载的宽度,应该接近它在页面上的显示宽度,而不是等于原图宽度。内容区最大宽度改了,这里要同步改。
正文图片默认懒加载是合理的;首屏大图不行——它被推迟到布局完成之后才加载,首屏就先白一块。
模板里能显式写属性的地方,显式写:
echo wp_get_attachment_image( $hero_id, 'hero-1600', false, [
'loading' => 'eager',
'fetchpriority' => 'high',
'decoding' => 'async',
'sizes' => '(max-width: 900px) 100vw, 900px',
'class' => 'hero-img',
] );只有在改不了模板时,才用过滤器排除正文首图:
add_filter( 'wp_lazy_loading_enabled', function ( $default, $tag_name, $context ) {
if ( 'img' !== $tag_name || is_admin() || is_feed() ) {
return $default;
}
static $first = true;
if ( $first ) {
$first = false;
return false;
}
return $default;
}, 10, 3 );这里靠渲染顺序判断「第一张」,如果页头 logo 先渲染,被放过的就是 logo 而不是正文首图。能写属性就别用这段。
# WebP 协商是否生效
curl -sI -H 'Accept: image/webp' https://example.com/wp-content/uploads/2026/10/photo.jpg | grep -i '^content-type'
# 文件是否真的存在
curl -sI https://example.com/wp-content/uploads/2026/10/photo.jpg.webp | head -n 1
# 页面里的 img 标签
curl -s https://example.com/sample-page/ | grep -o '<img[^>]*>' | head -n 5再加两个肉眼检查:首图是不是 eager 加 fetchpriority=high;把网络限速到慢速 3G,看首屏是否先出图。
顺序错了的典型现象:
sizes 没跟着布局改。改完想整体过一遍线上表现,可以用站点检查工具跑一次:https://aumcreate.com/tools/site-check?utm_source=tencent&utm_medium=content&utm_campaign=tencent-image-webp-lazy-order
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。