图片才是个人站最大的性能负债
做过 Lighthouse 体检的站长都有体会:一张没有压缩的 PNG 截图动辄一两兆,一个列表页塞十几张配图,页面体积轻松破十兆,首屏 LCP 直接被拖到四五秒。文字和 HTML 压缩来压缩去省下的那几十 KB,在一张没优化的主图面前不值一提。图片优化几乎是投入产出比最高的性能改造成本——不用改架构,不用加机器,处理完体积能砍掉一半以上。
本文讲一条完整的落地链路:本地批量把 PNG/JPEG 转成 WebP 和 AVIF,页面用 让浏览器自动挑选格式,配 Nginx 做兜底的动态转换和长缓存,最后用响应式 srcset 让手机不下载桌面大图。
一、先搞清楚三种格式的取舍
- JPEG:兼容性天花板,任何浏览器都认,但没有透明通道,压缩到极致会有可见块效应。
- WebP:同时支持有损、无损和透明通道,同等画质下比 JPEG 小 25% 到 35%,现在已经全平台覆盖(含 Safari 14 以上)。
- AVIF:基于 AV1,同画质下比 WebP 还能再小 20% 到 30%,尤其适合渐变和细节丰富的照片,但编码慢、老设备解码要吃 CPU,兼容性略逊于 WebP。
结论很清楚:三种都出,页面里用 按 AVIF、WebP、JPEG 的顺序声明,浏览器从上往下挑第一个认识的。这样高版本浏览器吃到最小体积,老设备退回 JPEG,谁都不吃亏。
二、本地批量转换:用 sharp 一次搞定
ImageMagick 转换 AVIF 的速度很感人,几百张图能等到天亮。Node 生态里的 sharp 基于 libvips,多线程、内存占用低、速度快一个数量级,是批量转换的首选。
mkdir imgopt && cd imgopt
npm init -y
npm install sharp glob写一个转换脚本,读取源图目录,为每张图同时生成 WebP 和 AVIF:
const sharp = require("sharp");
const { glob } = require("glob");
const fs = require("fs");
const path = require("path");
const SRC = "./src";
const OUT = "./dist";
const MAX_WIDTH = 1600; // 超过则不放大,只等比缩小
async function run() {
const files = await glob(`${SRC}/**/*.{jpg,jpeg,png}`);
for (const f of files) {
const name = path.basename(f, path.extname(f));
const rel = path.relative(SRC, path.dirname(f));
const outDir = path.join(OUT, rel);
fs.mkdirSync(outDir, { recursive: true });
const img = sharp(f, { failOn: "none" }).rotate(); // rotate() 按 EXIF 自动转正
const meta = await img.metadata();
const width = Math.min(meta.width || MAX_WIDTH, MAX_WIDTH);
// WebP
await img.clone().resize({ width }).webp({ quality: 78, effort: 5 })
.toFile(path.join(outDir, `${name}.webp`));
// AVIF(quality 稍低,因为 AVIF 同质量观感更好)
await img.clone().resize({ width }).avif({ quality: 50, effort: 4 })
.toFile(path.join(outDir, `${name}.avif`));
console.log("done", name);
}
}
run();几个参数的经验值:WebP 的 quality 落在 75 到 82 之间肉眼看不出区别;AVIF 的 quality 不要照着 WebP 的数值填,40 到 55 就已经很好了,填 78 会让文件比 WebP 还大。effort 越高压缩率越好但越慢,批量处理时 4 到 5 是合理区间。
三、页面里怎么写 picture 和 srcset
单图的标准写法,注意格式顺序和 type 声明:
三个细节值得强调。第一,width 和 height 一定要写,它们是给浏览器算宽高比用的,能消除图片加载时的布局跳动(CLS),这对 Core Web Vitals 是硬指标。第二,首屏主图不要加 loading="lazy",否则会延迟 LCP,只有折叠线以下的图才懒加载;首屏图建议用 fetchpriority="high"。第三,alt 别留空,既是无障碍要求也是图片 SEO 的基本功。
如果要喂响应式多尺寸,用 srcset 加 sizes:
sizes 是告诉浏览器"这张图在页面上会占多宽的视口",浏览器据此结合设备像素比算出该下载哪个候选。写错了会导致手机去下 1600 宽的大图,那就白优化了。生成多尺寸时把上面的 sharp 脚本包一层宽度循环即可。
四、Nginx 兜底:动态转 WebP 与长缓存
历史图片量大、不想全量重新生成的情况下,可以让 Nginx 按 Accept 头动态返回 WebP。思路是给静态图加一条判断,浏览器支持 WebP 就回落到 .webp 文件(前提是你已经用 cron 提前生成好):
map $http_accept $webp_suffix {
default "";
"~*image/webp" ".webp";
}
server {
listen 443 ssl http2;
server_name www.zz1984.com;
location ~* ^/img/(.+)\.(png|jpe?g)$ {
root /var/www/site;
try_files /img/$1$webp_suffix /img/$1.$2 =404;
# 图片一律长缓存 + 内容哈希版本号
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# AVIF 单独处理,注意有些老客户端会声明多个 Accept
location ~* ^/img/(.+)\.(png|jpe?g)$ {
add_header Vary Accept;
}
}关键点是 add_header Vary Accept。没有它,CDN 或中间代理会把第一个客户端拿到的格式缓存下来发给定所有人——一个只支持 JPEG 的爬虫访问过之后,所有用户都会收到 JPEG,优化全废。CDN 侧如果有自动格式转换功能(很多免费 CDN 都带),优先用它,它能在边缘节点自动转 WebP/AVIF,比源站转更省事。
五、别忘了缓存策略与验证
图片文件名带内容哈希是长缓存的前提,改了图必须改文件名,否则用户永远拿到旧图。如果做不到哈希文件名,退一步用较短的 max-age 加 ETag 协商。上线后怎么验证真的生效:
# 看服务端实际返回的格式
curl -s -o /dev/null -D - -H "Accept: image/avif,image/webp,*/*" \
https://www.zz1984.com/img/cover.png | grep -i "content-type\|vary"
# 看单张图优化前后体积对比
ls -lh src/cover.png dist/cover.webp dist/cover.avif用 Chrome DevTools 的 Network 面板过滤 Img,把 "Size" 列排序,最大的那几张就是下一个优化目标。Lighthouse 报告里的 "Serve images in modern formats" 和 "Properly size images" 两个诊断项清零,基本就说明图片这块做干净了。
结语
图片优化没有玄学,就是三步:转格式、给多种尺寸、让浏览器自己挑。本地用 sharp 批量生成 WebP 和 AVIF,页面用 picture 加 srcset 声明,Nginx 或 CDN 做兜底分发,再配上长缓存和正确的 Vary 头。做完这一轮,一个图片密集的列表页从十兆掉到两三兆是常态,LCP 改善肉眼可见。对个人站长来说,这是不需要花钱买服务器、纯粹靠改配置就能拿到的性能收益。