图片体积预算:把一万张配图压到十分之一

被"图片拖慢"的一台小机器

今年春天帮一个做菜谱的站做性能体检。站不大,四千多篇文章,每篇配三到五张成品图,日访问两万左右。服务器是 2 核 4G 的便宜 VPS,跑 Nginx + PHP-FPM + MySQL。站长说他已经开了 CDN、开了浏览器缓存、上了图片压缩插件,但 PageSpeed 移动端还是只有四十多分,LCP 稳定在四点五秒以上。

我先看了一眼他的流量账单:一天出网 90GB,其中 87GB 是图片。再看一眼图片总体积:四千篇文章、一万五千张图,加起来 6.8GB。平均每张 450KB。这就已经说明了问题——不是缓存没开,是每张图本身就太大了。

这次体检之后我整理出一套"图片体积预算"的做法,从定标准、测量、改造到防止回退,在几个站上跑下来,普遍的收益是把图片总体积压到原来的八分之一到十分之一,移动端 LCP 能降两秒左右。这篇按步骤讲。

先定预算,再谈优化

"图片要压缩"是句废话,因为它没给标准。没有标准就没有验收,改造完你还是不知道做到位没有。所以我做的第一件事是定一个可执行的预算表,按图片在页面上的实际显示宽度分档:

  • 首屏主图 / 文章头图:显示宽度通常是一个视口宽,移动端约 400px(2 倍屏 800px),体积预算 120KB。
  • 正文内插图:显示宽度约 700px(内容区),移动端约 350px,预算 100KB。
  • 缩略图 / 列表页:显示宽度 200~300px,预算 25KB。
  • 图标、装饰性小图:预算 10KB,能换 SVG 的一律换 SVG。

这几个数字的依据是"在 4G 网络下,120KB 大约需要 0.3 秒传输"。一篇文章如果有三张 100KB 的插图,加上 HTML/CSS/JS 约 150KB,整页在 1.5MB 以内,配合 CDN 边缘缓存,LCP 有望压进 2.5 秒。超过这个量级,无论怎么优化服务器都很难救。

预算表要写进团队规范或者自己的推稿清单里,因为图片体积是唯一一个会随着发稿量线性膨胀的性能指标。代码写错了改一次就好,图片是每发一篇多几张,三个月不管,站就慢回来了。

测量:先看清钱花在哪

在动手之前,必须拿到三份数据。这三份都不需要装任何工具,用现成的命令行和 Nginx 日志就能拿到。

第一份,总量与分布。把图片目录整体扫一遍,按体积排序,看最大的那几张占了多少:

cd /var/www/site/uploads
du -sh .
find . -type f \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.webp' \) -printf '%s %p\n' \
  | sort -rn | head -30 | awk '{printf "%.1fMB %s\n", $1/1048576, $2}'

这一步经常出现一个规律:前 5% 的图贡献了 50% 的体积。菜谱站那次排下来,最大的二十张图每张 3~6MB,都是站长直接从相机导出的原图,忘了压就传了。这二十张图单独处理,收益比全站批量压缩还大。

第二份,类型分布。用 Nginx 日志看实际被请求的图片类型和平均大小:

awk -F'"' '/\.(jpg|jpeg|png|webp|gif|avif)/ {
  split($2, a, " ");
  print a[2]
}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

如果日志里还有人大量请求 .png 的照片,那就明确了:照片用 PNG 是纯粹的体积浪费,一张同样画质的照片,WebP 大约只有 PNG 的三分之一。这个问题在"截图工具默认存 PNG"的人身上特别普遍。

第三份,首屏那几张到底多大。取三篇有代表性的文章,把正文里的图片 URL 全列出来,逐个 HEAD 一下看 Content-Length。这份数据决定优先级——首屏 120KB 的预算超了多少,是最直接影响 LCP 的部分。

三份数据一起看,优先级基本就定了:先处理异常大图,再处理格式不对的,最后做全站批量。

格式选择:把 2026 年的现实讲清楚

格式这块网上的信息很乱,很多还是五年前的说法。按现在的浏览器支持度排一下:

AVIF 压缩率最好,同画质下比 WebP 再小 30~50%,但编码很慢(一张图几秒),且老设备解码慢。适合"一次性生成、长期缓存"的场景,比如文章配图。软编速度是主要痛点,硬编要新一点的 CPU。

WebP 是目前最稳的默认选择。Chrome、Safari 14+、Firefox 全支持,同画质比 JPEG 小 25~35%,编码速度是 AVIF 的十倍以上。我的默认策略是:**能在 WebP 和 AVIF 之间选,就优先 WebP;只在体积敏感的首屏主图上额外生成 AVIF 版本**。

JPEG 仍然是兜底。用 MozJPEG 编码的 JPEG 比标准 libjpeg 小 10% 左右,是"必须保留 JPEG"时的最佳选择。

PNG 只用于三种情况:需要透明通道、需要无损(截图里的文字)、图很小(图标)。照片存 PNG 是明确的错误,没有例外。

实操上最省事的方案是用 <picture> 给浏览器做选择,让它自己挑:

<picture>
  <source type="image/avif" srcset="/img/abc-800.avif 800w, /img/abc-1600.avif 1600w" sizes="(max-width:600px) 100vw, 800px">
  <source type="image/webp" srcset="/img/abc-800.webp 800w, /img/abc-1600.webp 1600w" sizes="(max-width:600px) 100vw, 800px">
  <img src="/img/abc-800.jpg" width="800" height="533" loading="lazy" decoding="async" alt="成品图">
</picture>

三个细节不能漏:width/height 必须写(否则会引起布局偏移,CLS 直接爆表);sizes 必须和实际 CSS 显示的宽度吻合(写错了浏览器会挑错档位,等于白做);<img> 的 src 要指向一个真实存在的 JPEG,它是所有老浏览器的兜底。

压缩参数:那五个真正影响体积的旋钮

用 cwebp 举例,参数一大堆,但真正影响"体积/画质"平衡的只有五个:

cwebp -q 78 -m 6 -mt -sharp_yuv -metadata none input.jpg -o output.webp
  • -q 78:质量。照片类 75~80 是甜蜜点,低于 70 开始出现明显的块状噪点,高于 85 体积增长很快而肉眼几乎看不出差别。文字截图要用 90 以上。
  • -m 6:压缩方法(0~6),6 最慢但最小。批量处理时用 4 能省一半时间,体积多 2~3%。
  • -mt:多线程,批量处理必开。
  • -sharp_yuv:色彩转换时用更锐利的算法,明显减少红蓝色块。这个参数几乎无代价,必开。
  • -metadata none:剥掉 EXIF。这是最容易被忽略的一条——相机导出的照片 EXIF 里可能有缩略图、GPS 坐标、设备信息,光 EXIF 就几十到几百 KB。而且 GPS 信息是隐私问题。

AVIF 用 avifenc,质量用 -q 60 起步(AVIF 的质量刻度不同,60 大约相当于 WebP 的 78),速度用 -s 6。速度调到 8 以上编码时间会显著变长,批量跑要算好机器负载。

还有一个常被讨论但收益有限的参数:缩放到"合理的最大宽度"。如果你的内容区最大宽度是 800px,那么上传 2400px 宽的图毫无意义(2 倍屏 1600px 就到顶了)。先裁尺寸再压质量,顺序不能反——把 2400px 压到 q78 再缩到 800px,得到的文件比先缩到 800px 再压 q78 要大得多,因为压缩算法在多余像素上浪费了码率。

响应式图片:别让手机下载桌面版的图

移动端占一个内容站流量的七成以上。如果手机下载的是桌面尺寸的图,等于凭空多出两倍流量。这是"图片预算"里最容易被漏掉、收益又最大的一环。

做法是给每张图生成三个尺寸:

# 以 1600px 原图为输入,生成三档
for w in 400 800 1600; do
  ffmpeg -i src.jpg -vf "scale=$w:-2" -q:v 3 tmp_$w.jpg
  cwebp -q 78 -sharp_yuv -metadata none tmp_$w.jpg -o out_$w.webp
done

然后 srcset 里列出三档,配 sizes 让浏览器按视口和设备像素比自选。这里有个反直觉的点:不要让 srcset 里的档位太多。三到四档足够,档位多了浏览器反而可能选到次优的一档,而且生成的存储成本上升。sizes 的写法也要照着实际布局写,比如正文图的容器是 max-width: 800px 且左右有内边距,那 sizes="(max-width: 880px) calc(100vw - 40px), 800px" 才是准确的。

还有一个细节:首屏图不要 lazy。loading="lazy" 会让浏览器推迟加载视口外的图,但首屏那张必须立刻加载——给它加 fetchpriority="high",同时把它从 lazy 里拿出来。反过来,正文里靠下的图全部 loading="lazy",这个组合对 LCP 的影响比图片压缩本身还直接。

改造流程:一次性的批量处理怎么做才不翻车

存量图多的站,批量处理是必须的,但直接在原目录上覆盖是灾难性的做法(一旦参数错了,原图没了,只能重拍)。我用的流程是四步:

第一步,全量备份。不用多说,rsync -a uploads/ uploads.bak/,跑完核对文件数一致。

第二步,生成到新目录。所有输出写进 uploads-opt/,保持原有的子目录结构,方便后面整体替换。

第三步,生成对照报告。csv 里记录每个文件的原始体积、新体积、尺寸、是否跳过。跑完看一眼:跳过的是不是都是本来就很小的图,体积反而变大的有没有(极少见但确实会发生,比如原本已经是高质量 WebP)。

第四步,灰度替换。不要直接 mv 整个目录。先改 Nginx 把 /uploads/ 指向新目录跑一天,观察错误日志和 404 数量;确认没问题再正式切换,旧目录保留一周后删除。

批量脚本的并发度要控制住。用 xargs -P 4 而不是 -P 16——编码是 CPU 密集任务,并发开到核数以上只会互相抢时间,还容易把机器上的 PHP-FPM 拖到超时。跑之前先用 nice -n 10 降优先级,半夜跑。

另外,动图(GIF)单独处理。GIF 转 WebP 或视频能减掉 90% 体积,但转换会丢掉有些编辑器的兼容性。我一般直接把大的 GIF 换成 <video autoplay muted loop playsinline> 配 mp4/webm,效果一样,体积小一个数量级。

防回退:让新发的图不再超标

存量改造只是一次性的,真正决定长期水平的是"新图进来时能不能拦住大图"。三道闸门,按成本从低到高:

闸门一,上传时自动处理。把"上传即压缩"做成一条管线:接收文件 → 按 EXIF 方向校正 → 缩到最大宽度 1600 → 生成三档 WebP(可选 AVIF)→ 剥 EXIF → 写入。这条管线用 PHP 的 Imagick 或直接调 cwebp 都行。关键是不要给用户"上传原图"的选项,默认就是处理过的版本。

闸门二,发布前的体积校验。在编辑器/后台保存文章时扫一遍正文里的 <img>,对每个 URL 做一次 HEAD 拿 Content-Length,超过预算就在后台亮黄条提示"这张图 480KB,建议压缩"。这是"提醒"不是"阻止",因为偶尔确实需要一张高质量大图。

闸门三,每日巡检。一条 cron,扫出最近 24 小时新增的图片里超过 300KB 的,发一封汇总邮件或者写进站点后台的运维面板。这个我最推荐,成本最低,而且能形成习惯——看到日报里出现大图,当天就处理掉。

find /var/www/site/uploads -type f -newermt '-1 day' \
  -size +300k -printf '%s %p\n' | sort -rn | head -20

验证:看数字,不看感觉

做完改造之后,必须量化验证,否则你不知道收益有多大、有没有遗漏。看三个指标:

指标一,单页图片总字节。用 DevTools 的 Network 面板按 Img 过滤,看 transferred 总量。改造前后各取同一篇文章对比。菜谱站那次,一篇三图文章的图片总量从 1.4MB 降到 156KB。

指标二,LCP。用真实用户数据(CrUX 或者自建 RUM),而不是本地 lab 数据。lab 数据受本机网络影响太大,看不出 CDN 和移动网络的真实表现。改造后观察一周的 p75 变化,比单次测速可靠得多。

指标三,CDN 出网流量。这是最直接的省钱指标。改造后一周对比前一周的图片流量,如果降了 80%,说明改造真的生效了;如果只降了三成,那说明还有大量页面在用旧地址。

最后一个容易踩的坑:改造后一定要清理一次 CDN 缓存。图片 URL 没变但内容变了,CDN 上还是旧的 450KB 版本,你会以为改造失败。稳妥的做法是换地址(加 -v2 后缀或者换目录),而不是覆盖原文件——这样既能强制回源,也不会有缓存不同步的问题。

一些不值得投入的方向

图片优化这块的坑很多,有几个方向投入产出比很低,提前说清楚能省不少时间:

  • 追求极致压缩率。从 q78 再压到 q70,体积能再降 15%,但画质损失肉眼可见,用户会抱怨"图片糊了"。这个取舍对内容站不划算,画面质量本身就是内容的一部分(做菜谱站尤其如此)。
  • 批量转 AVIF。编码慢、老设备解码慢,而收益相比 WebP 只是锦上添花。只在首屏主图上做,不要全站铺开。
  • 上图片处理 SaaS。如果你的图只有几千张,一次性批量处理 + 上传管线就够用了,没必要为几十万张图才划算的服务付费。
  • 用 JS 做懒加载。原生 loading="lazy" 已经覆盖了绝大多数场景,再叠一层 IntersectionObserver 只会增加复杂度和首屏阻塞。

回头看那台 2 核的小机器,最后做了什么?把最大的二十张原图重压(省 3.1GB 存量里的 900MB),全站批量转 WebP + 剥 EXIF + 限制最大宽度 1600(6.8GB 降到 780MB),正文图全部换成三档 srcset,首屏图加 fetchpriority="high" 并从 lazy 里移出,上传管线改成自动处理。图片出网从日 90GB 降到 11GB,移动端 LCP 从 4.5 秒降到 2.2 秒,PageSpeed 从 42 分到 87 分。服务器一个字没改,硬件一分钱没加。图片体积预算是性价比最高的一档性能优化,因为它几乎不涉及架构,只涉及习惯。

Last modification:October 26th, 2026 at 10:27 pm

Leave a Comment