网站图片太多怎么优化保存

文章导读
图片多的站,最先要定的是三件事:原图放哪、交付图怎么生成、URL 怎么给前端。原图留一份不动,交付图按尺寸和格式预先生成到独立目录、路径可推导,前端照旧引用原图地址,由 nginx 决定回哪一张。这样后面换成 WebP、增加尺寸档、接 CDN 都只在派生层动,不用回头改数据库里的图片字段,也不用批量替换模板。
📋 目录
  1. A 网站图片太多之前先确认这三件事
  2. B 网站图片太多:最短可用路径
  3. C 网站图片太多:一个真实场景的完整做法
  4. D 网站图片太多:跑不通先查这三处
A A

图片多的站,最先要定的是三件事:原图放哪、交付图怎么生成、URL 怎么给前端。原图留一份不动,交付图按尺寸和格式预先生成到独立目录、路径可推导,前端照旧引用原图地址,由 nginx 决定回哪一张。这样后面换成 WebP、增加尺寸档、接 CDN 都只在派生层动,不用回头改数据库里的图片字段,也不用批量替换模板。

判断顺序是:先确认图片从哪个入口出去(源站直连还是 CDN)、图像处理工具支不支持 WebP,再决定派生图什么时候生成:存量图一次性批量预生成,新图在上传环节同步生成。交付层优先用独立 URL 区分格式;如果要用同一个 URL 靠 Accept 协商,必须带上 Vary,并确认 CDN 缓存键包含该请求头。原图务必单独备份,派生图可以随时重算,不要为了省空间删原图。

网站图片太多之前先确认这三件事

这三件事任何一件没确认,后面批量处理都会卡住,而且卡住的地方通常和图片本身无关。

版本与模块

nginx -v 看版本号,nginx -V 看编译参数里有没有 with-http_image_filter_module;没有这个模块,就别指望在 nginx 里做动态裁剪,只能预生成。图像工具用 magick -version(ImageMagick 7)或 convert -version(ImageMagick 6),再执行 magick -list format | grep -i webp 确认能不能输出 WebP;格式列表里没有 webp,说明编译时没带 libwebp,可以改用 cwebp -version 那条路。程序侧执行 php -m | grep -iE 'imagick|gd' 看扩展装没装。

权限

ls -ld /var/www/site/uploads 看属主和权限位,ps -eo user,comm | grep nginx 看 worker 实际跑在哪个用户下。批量脚本如果以 root 跑,生成出来的文件权限通常没问题,但目录模式是 700 时 nginx 会直接 403;生成完统一 chown 到 nginx 用户更稳。

入口位置

用 nginx -T | grep -nE 'server_name|root|location' 找到真正生效的配置文件和 location 顺序,注意 include 的先后。走 CDN 的,去控制台的缓存规则、缓存键页面确认是否按完整 URL 缓存、是否忽略查询串。最后抽几条数据看字段:SELECT image_url FROM articles LIMIT 5; 返回的是绝对 URL 还是 /uploads/... 相对路径,这决定你能不能把图片切到独立域名。

网站图片太多:最短可用路径

五步跑通。路径假设源图在 /var/www/site/uploads,派生图放同级 uploads-derived,站点 root 是 /var/www/site,换成自己的再执行。

第一步,盘点。

cd /var/www/site/uploads
find . -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) | wc -l
du -sh .

该看到文件总数和总体积。数量和数据库里的图片记录数差很多,先查孤儿文件和外链图,别急着压。

网站图片太多怎么优化保存

第二步,备份。

rsync -a /var/www/site/uploads/ /backup/uploads-$(date +%F)/

rsync 正常退出后,du -sh /backup/uploads-* 应该和源目录体积接近。

第三步,生成派生图。

#!/usr/bin/env bash
set -euo pipefail
SRC=/var/www/site/uploads
DST=/var/www/site/uploads-derived
find $SRC -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) -print0 |
while IFS= read -r -d '' f; do
  rel=${f#$SRC/}
  out=$DST/$rel.webp
  mkdir -p $(dirname $out)
  if [ -f $out ]; then continue; fi
  magick $f -auto-orient -resize '1600x1600>' -quality 82 $out
done

存成 gen-webp.sh。输出名是 a.jpg.webp,故意保留原扩展名,好和下面的 nginx 规则对上。-resize '1600x1600>' 只缩不放,-quality 82 是起点,按实际画面再调。脚本可重跑,已有的派生图会跳过。跑完用 find $DST -name '*.webp' | wc -l 对比源图数量,再 magick identify 抽一张看尺寸和格式。

第四步,nginx 加规则。

# /etc/nginx/conf.d/img-webp.conf
map $http_accept $webp_ok {
    default 0;
    ~*image/webp 1;
}
# 站点配置文件里的 server 块内
location ~* ^/uploads/(?<rel>.+\.(?:jpe?g|png))$ {
    root /var/www/site;
    set $target /uploads-derived/$rel.webp;
    if ($webp_ok = 0) { set $target /uploads/$rel; }
    try_files $target =404;
    add_header Vary Accept;
    expires 30d;
}

改完执行 nginx -t && systemctl reload nginx,看到 syntax is ok 和 test is successful 才算过。

第五步,验证。

curl -sI -H 'Accept: image/webp' https://img.example.com/uploads/2024/05/a.jpg | grep -iE 'content-type|content-length|vary'

该看到 Content-Type: image/webp 和 Vary: Accept;去掉 -H 'Accept: ...' 再跑一次,应该回到 image/jpeg。

网站图片太多怎么优化保存

网站图片太多:一个真实场景的完整做法

场景:一个内容站,/var/www/site/uploads 下存量几万张 jpg/png 商品图,按 年/月/ 分目录,模板里直接写死 /uploads/2024/05/a.jpg,改模板成本高。于是走同 URL + Accept 协商,由 nginx 决定给 WebP 还是原图。

每项配置的具体归属:

  • 派生目录 /var/www/site/uploads-derived:写在生成脚本的 DST=。放 uploads 同级而不是内部,避免被后台的目录清理任务当成垃圾删掉。
  • 最大边 1600、质量 82:写在脚本的 -resize 与 -quality 参数上。详情大图常用 1200-1600,列表缩略图另跑一档 400 的,别一档走天下。
  • map 与 location:map 放 /etc/nginx/conf.d/img-webp.conf,location 写进站点配置文件,实际路径以 nginx -T 输出为准。
  • 缓存时间 expires 30d:写在 location 内。图片按日期分目录、文件名不变时不要设太长,否则替换图片后用户还在看旧图。
  • 目录属主:生成后执行 chown -R www-data:www-data /var/www/site/uploads-derived,用户名以 ps -eo user,comm | grep nginx 的结果为准。

执行顺序是:先挑 2024/05 一个子目录跑脚本,确认产物名和 nginx 拼出来的路径一致;reload 后对该目录的一张图 curl 验证;通过后再全量跑 nohup bash gen-webp.sh > /tmp/gen.log 2>&1 &,用 tail -f /tmp/gen.log 盯进度。中断了直接重跑,已生成的会跳过。

容易踩的坑出在缓存这一层。同一个 URL 返回两种格式,如果 CDN 或前置代理只按 URL 做缓存键,会把 WebP 响应发给不支持 WebP 的客户端,表现是部分用户图片打不开、另存下来是空白文件;反过来则是缓存了 jpg,白转一趟。两种处理:在 CDN 缓存键里加上 Accept 请求头(多数 CDN 的缓存键配置页支持),或者干脆让派生图用独立 URL /uploads-derived/2024/05/a.jpg.webp,由模板或后端输出,不再依赖协商。改完清一次对应路径的缓存,再用两种 Accept 各请求一次。

网站图片太多:跑不通先查这三处

一、图片 404。error.log 里会是 open() /var/www/site/uploads-derived/2024/05/a.jpg.webp failed (2: No such file or directory)。查两点:产物文件名到底是 a.jpg.webp 还是 a.webp(脚本写成 $DST/${rel%.*}.webp 就会和 location 里的 $rel.webp 对不上,这是最常见的一处不匹配);以及 try_files 拼出的绝对路径和 root 是否一致,用 nginx -T | grep -n root 核对。

二、403 或脚本报 Permission denied。出现 failed (13: Permission denied) 或 magick 打不开文件时,查三层:ls -ld /var/www/site /var/www/site/uploads-derived(父目录也要有 x 权限)、id www-data 确认 nginx 用户、开了 SELinux 的机器用 ls -Z /var/www/site/uploads-derived 看上下文,必要时 restorecon -Rv。

三、转完了还是返回 jpeg。curl -sI -H 'Accept: image/webp' 回来仍是 Content-Type: image/jpeg。先 nginx -T | grep -n -A3 'map $http_accept' 确认 map 写在 http 块内、conf.d 确实被 include;再看有没有更靠前的正则 location 抢先匹配,nginx -T 的输出顺序就是匹配优先级;最后排除 CDN 缓存了旧响应,用带查询串的地址试一次(?v=1 是另一个缓存键,只用于验证)。