图片体积预算:把一万张配图压到十分之一并防止回涨

图片文件不大,但请求数多,而且几乎每个页面都有。很多人做过一次「压缩图片」之后就以为完事了,半年后又发现首页传输量回到原点——因为新文章在持续上传,旧文章在持续被引用,图片这件事是会「长回去」的。这篇文章讲的是怎么给图片体积定一个预算,然后用一条可重复的管线把总量长期压住,而不是做一次性的批量压缩。

一、先算清楚现在的量:三个数字

做任何优化之前先量化。图片这块只需要三个数字:图片总量(字节)、单页图片请求数与传输量、以及图片请求占总请求与总字节的比例。前两个从服务器文件和访问日志都能拿到,第三个决定了这件事值不值得做。

find /www/wwwroot/uploads -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) \
  -printf '%s\n' | awk '{s+=$1; n++} END{printf "%d files, %.1f MB\n", n, s/1048576}'
grep -E '\.(jpg|jpeg|png|webp|gif)' /var/log/nginx/access.log \
  | awk '{b+=$10; n++} END{print n, b/1048576 " MB"}'

经验值:内容站的图片通常占页面总字节的 50% 到 75%,占请求数的 30% 到 50%。如果算出来只有 10%,说明你的站可能以文字为主,优先级应该放在别处,不必在这里花力气。如果超过 70%,那么图片就是性能的主要矛盾。

二、预算怎么定:按页面平均分配,而不是按单张

「每张图不超过 200KB」这种规则没有意义,因为一个页面可能有 3 张图也可能有 30 张。合理的预算单位是「单页图片总量」,比如正文首图 120KB、内文图每张 60KB、缩略图每张 15KB,按一个典型页面 8 张图算,总量约 500KB。这个数字再和你的月流量、CDN 成本对一下,就能判断是否可行。

定的过程里有两个反直觉的点。第一,质量参数不是线性的:JPEG 从 90 降到 82,体积往往掉 40%,肉眼几乎看不出来;从 82 降到 75,体积再掉 20%,但文字边缘开始模糊。所以 82 是大多数内容站 JPEG 的甜点。第二,现代编码器(mozjpeg、libwebp)在同等质量下比默认参数小 20%~30%,换编码器的收益往往比调质量参数更大。

三、管线第一层:上传时就转,不要事后补

所有「事后批量压缩」的方案都会随时间失效,因为新图持续进来。正确的位置是在上传环节做转换:文章发布时把原始图存一份归档(不进 Web 目录),同时生成 2~3 个尺寸的 WebP/AVIF 派生文件。派生文件的命名规则要固定,比如 name-800.webp、name-1600.webp,模板里用 srcset 让浏览器自己挑:

<img src="/uploads/2026/11/pic-800.webp"
     srcset="/uploads/2026/11/pic-800.webp 800w,
             /uploads/2026/11/pic-1600.webp 1600w"
     sizes="(max-width: 800px) 100vw, 720px"
     width="1600" height="900" loading="lazy" decoding="async" alt="描述">

width 与 height 必须写,这不是可选项——缺了它们浏览器无法预留空间,图片加载完成时会推动下方内容,累积布局偏移(CLS)直接超标。这是图片处理里最容易被忽略、收益却最确定的一条。

如果用的是现成 CMS,不想改上传逻辑,退一步的替代方案是:上传时只做「体积检查 + 强制转 WebP」,由 nginx 的 try_files 在请求时判断是否存在对应 WebP 并回退到原图。这样既保证新内容不进胖子,也不用改模板的 img 标签。

四、管线第二层:批量清理历史图片,分三批做

历史图片量大,一次全转风险高:可能误伤被外部引用的图、可能把一个仍在被访问的路径改坏。稳妥的做法是分三批,每批都保留可回滚能力。

第一批只做「统计不改动」:扫描全部图片,输出体积排行、格式分布、以及最近 90 天是否被访问过的标记。第二批处理「体积大于 300KB 且近 90 天有访问」的图片——这批收益最高、风险最低。第三批处理「近 90 天零访问」的图片:这些不压缩,直接考虑归档到冷存储或删除,因为它们只占磁盘不产生流量,压缩它们没有意义。

# 从访问日志统计 90 天内被访问过的图片路径
awk '{print $7}' /var/log/nginx/access.log.1 /var/log/nginx/access.log \
  | grep -E '\.(jpg|jpeg|png)$' | sed 's/?.*//' | sort -u > visited.txt
# 找出上传目录里没被访问过的
find /www/wwwroot/uploads -name '*.jpg' -printf '%p\n' \
  | sed 's|/www/wwwroot||' | sort > all.txt
comm -23 all.txt visited.txt | wc -l

comm 要求两个文件都排好序,这是最容易出错的地方。跑出来的行数就是「可以归档」的候选,但删之前一定要再看一眼:有些图是本站页面里引用的但访问量极低(比如只有搜索引擎抓过一次),删掉会变成 404。保守做法是移动到一个 /uploads/_archive/ 目录并返回 410,观察一个月,没问题再真正删除。

五、管线第三层:格式选择不要迷信单一答案

WebP 的兼容性现在基本没有障碍,但它对照片类图片的优势远大于对截图和线条图形的优势。实际测试中,照片类图片 WebP 比 JPEG 小 25%~35%,而 UI 截图、图表、纯色 logo 这类图片,WebP 反而可能比优化过的 PNG 更大。所以格式策略应该是按内容类型分:

照片、渐变背景、含噪点的插画用 WebP(或 AVIF,如果你能接受稍长的编码时间);截图、图表、带文字的图用 PNG 或者无损 WebP;图标、logo、单色图形用 SVG,别用位图。这条规则落到管线里就是在转换脚本里加一个判断:读图片的调色板数量与边缘特征,或者更简单地按来源目录(screenshots/ 与 photos/)分别走不同的编码器。

六、派生图的存放与失效:三个必须想清楚的问题

管线一旦开始生成派生文件,就绕不开三个问题。第一是目录膨胀:一张原图配三个尺寸三个格式就是九个文件,一万篇带图文章下来,光文件数就能让 ls 都卡住,ext4 的目录索引也会吃内存。解决办法是按年月分目录(uploads/2026/11/),单目录文件数控制在几千以内。

第二是删除与更新。用户在后台删了一篇文章,或者替换了一张图,如果派生文件没跟着走,就会留下永久孤儿文件,越积越多。更麻烦的是替换:原图换了但派生图还是旧的,页面显示的是旧内容而你在后台看到的是新图,这类问题极难被发现。稳妥的做法是派生文件名里带上原图内容哈希的前八位,比如 pic-a1b2c3d4-800.webp,哈希一变文件名就变,缓存与孤儿问题同时解决;清理脚本按「文件名里的哈希与原图不符」逐个删即可。

第三是 CDN 缓存。派生图上了长缓存之后,替换原图不会让 CDN 上的旧文件失效。带上哈希的命名让这个问题消失,因为你请求的是全新 URL。这也是为什么「文件名不变、只换内容」的图片更新策略在 CDN 时代基本不可用——它不是慢,而是错。

七、横向对比:一次真实的整改前后

拿一个实际的内容站举例。整改前:一万两千张图片,总计 3.4GB,平均单张 290KB;首页图片 14 张、传输 1.9MB;移动端完全加载时间 6.8 秒。整改分三步:第一步把历史大于 300KB 且近 90 天有访问的 3800 张转 WebP(质量 82,最长边 1600px),总量降到 1.2GB;第二步模板改成 srcset 加 width 与 height,首页图片从 14 张减到 6 张(折叠线以下的图改为点击展开);第三步加上一年期缓存头与 CDN。

结果是首页图片传输量从 1.9MB 降到 340KB,移动端完全加载时间从 6.8 秒降到 2.4 秒,CDN 图片流量费用下降约六成。这三步里收益最大的是第二步——减少图片数量和尺寸,比压缩已经不算大的图片有用得多。压缩省的是百分比,减量省的是绝对量,两者的收益不在一个量级上。

八、让预算长期生效:两个自动化的闸门

预算定完之后,靠自觉是保不住的。至少要有两个自动化的闸门。第一个是上传时的大小上限:超过阈值的图片拒绝上传或者自动降质重编码,并把结果写日志。第二个是每周一次的「超预算报告」:统计本周新增图片的平均体积、单页图片总量最大的十个页面、以及是否出现新的格式(有人上传了 BMP、TIFF 这类未经优化的格式)。

报告不需要多精美,一个脚本输出十行文本,通过 cron 发到邮箱即可。关键是让「体积回涨」这件事在你没注意的时候也能被看见。我自己的经验是,加了这个报告之后,三个月内新增图片的平均体积从 380KB 降到 110KB,主要不是因为脚本做了压缩,而是因为上传时被拦住这件事本身改变了习惯。

最后补一条容易被忽略的:CDN 与浏览器缓存对图片的收益远大于再过一遍压缩。同一张图片,加上 Cache-Control: public, max-age=31536000, immutable 之后,回访用户的图片请求基本全部命中本地缓存,实际传输量可以再降一半以上。压缩与缓存是两件不同的事,别只做了一件就收工。

Last modification:November 13th, 2026 at 07:25 pm

Leave a Comment