Featured image of post 博客封面格式升级实录:JPEG → WebP → AVIF

博客封面格式升级实录:JPEG → WebP → AVIF

把全站 73 张封面从 JPEG 换成 AVIF,体积从 40MB 降到 11MB。含完整可复现步骤、实测画质数据、六个踩过的坑,以及回退方法

   
文章摘要
全站 73 张封面从 JPEG 换到 AVIF(q60):40.1MB → 11.4MB,省 72%,画质 PSNR 反而更高。这篇把判断依据、转换脚本思路、上传与缓存坑、验证与回退全部写下来,照着能复刻

一篇写给自己看的操作笔记:怎么把博客封面从 JPEG 换成 AVIF,为什么选 q60、为什么阈值定 100KB、中途踩了哪些坑、出问题怎么退回去。

先说结论:73 张封面,40.1MB → 11.4MB,省 72%,画质 PSNR 反而更高。代价是老设备(iOS 16.4 以前)看不到封面——这个取舍后面单独说。

文中仓库名、域名、账号一律用占位符。

一、先搞清楚收益在哪

动手之前先测了三件事,避免白干:

  1. 换格式能省多少?把同一张原图分别存 WebP 和 AVIF,比体积和 PSNR
  2. 哪些图值得换?小图换完收益接近 0,还多一次请求开销
  3. 实际加载快多少?家里千兆宽带上测,避免拿理论值骗自己

第 3 条的结论有点反直觉:在快网下几乎测不出差别(WebP 组 0.57s vs JPEG 组 0.44s,波动比体积差异还大)。真正收益在慢网和流量成本——体积少了七成,弱网用户的首屏就不一样了。

封面格式转换流程

上面这张就是完整流水线,九步,每一步后面都会展开。

二、三种格式的实测数据

同一张原图(1456×816,JPEG 2046KB),各参数实测:

格式与参数对比

关键结论:

  • AVIF q60 同时赢了体积和画质:133KB / 37.1dB,比 WebP q82 的 161KB / 36.8dB 更小更清楚
  • q50 不能要:体积掉到 79KB,但 PSNR 掉到 34.8dB,属于"细看能看出"的档
  • q70 性价比差:183KB / 38.8dB,比 q60 大一截,画质只多 1.7dB

PSNR 参考区间:>40dB 肉眼几乎无差,35–40dB 很轻微,32–35dB 细看有差,<30dB 明显。所以 q60 是甜点。

三、阈值为什么定 100KB

一开始我按 >250KB 筛,只命中 47 张;把阈值下调到 100KB 后命中 73 张——也就是全站封面都得换。

原因很朴素:一张 100KB 的 JPEG 转 AVIF 大约能到 40–60KB,省下的 40KB 在移动网络上是实打实的;而 20KB 的小图转完可能只省 5KB,不够付多一次格式转换的维护成本。

所以判定规则就一条:原图 >100KB → 转;≤100KB → 放着。

四、完整步骤(照着做)

0. 工具准备

# WebP:Pillow 自带
python3 -c "from PIL import Image; print(Image.__version__)"

# AVIF:需要 libavif-bin(提供 avifenc / avifdec),约 2MB,无常驻
sudo apt install -y libavif-bin

注意:Pillow 读不了 AVIF。校验画质必须用 avifdec 转回 PNG 再比。

1. 扫描:找出哪些封面要转

封面信息我放在 static/images/random/<id>.json 里,用 HEAD 请求拿真实体积:

# 伪代码,核心是 HEAD 取 content-length,比下载全文快得多
for each 文章 -> 提取 front matter 的 imgid
  url = raw.githubusercontent.com/<repo>/master/paste/random-<id><ext>
  size = HEAD(url) 的 Content-Length
  如果 size > 100KB 且该 id 的 json 里还没有 avif 字段 -> 加入待转列表

用 ThreadPoolExecutor(max_workers=6) 并发,70 多张几秒钟扫完。

2. 下载原图

一定要走 raw.githubusercontent.com,不要走 CDN:CDN 可能有缓存滞后,拿到的是旧版本。

3. 编码

# AVIF(推荐参数)
avifenc -q 60 -s 6 input.jpg output.avif

# WebP(用 Pillow)
python3 -c "from PIL import Image; Image.open('in.jpg').save('out.webp','WEBP',quality=82,method=6)"

-s 6 是编码速度档(0 最慢最好,10 最快最差),6 在 N100 这种低功耗机器上单张 0.4–1.3 秒,够用。

4. 逐张比对体积(关键)

if 新文件大小 >= 原图大小:
    log("转完反而更大,跳过")
    continue

不做这步会踩坑——实测有两张图转 WebP 后分别大了 5% 和 20%(内容本身已经被压得很狠),硬用就是负优化。

5. 上传到 GitHub 图床

用 GitHub Contents API 的 PUT,注意文件已存在时必须带 sha:

body = {"message": "...", "branch": "master",
        "content": base64.b64encode(文件字节).decode()}
sha = GET /repos/<repo>/contents/<path>  # 存在才有
if sha: body["sha"] = sha
PUT /repos/<repo>/contents/<path>

不带 sha 会直接报 Invalid request. "sha" wasn't supplied.——重复跑脚本时一定会遇到。

另外:body 用临时文件 + --data-binary @file,别把 base64 塞进命令行参数,几十 KB 的图很容易超 ARG_MAX。

6. 刷 CDN 缓存

curl https://purge.jsdelivr.net/gh/<user>/<repo>@master/paste/random-<id>.avif

新文件在 jsDelivr 上不会立刻可见,purge 之后快很多。(jsDelivr 用 GitHub 作源,purge 完它才去回源。)

7. 在 json 里记录,不是替换

每个 id 的 json 保持多格式并存:

{
  "ext": ".jpg",
  "webp": "https://<cdn>/paste/random-003.webp",
  "avif": "https://<cdn>/paste/random-003.avif"
}

原图字段 ext 和 webp 一个都不删。好处是回退只需要改一个字段,不用重新转。

8. 文章 front matter 指向新格式

imgid: 003
image: https://<cdn>/paste/random-003.avif

默认用干净 URL(不加 ?v=),这样浏览器、CDN 边缘都能正常缓存,日志也清爽。

只有当某张图被替换过(同名文件内容变了),才临时加一个 ?v=<纳秒> 强制刷新,刷新完可以再摘掉。原因见下一节。

9. 模板自动化(Hugo archetype)

让以后 hugo new 自动优先选 AVIF,没有才退回 WebP / 原格式:

{{ $avif := index $json "avif" }}{{ $webp := index $json "webp" }}
{{ if $avif }}用 avif{{ else if $webp }}用 webp{{ else }}用原格式{{ end }}

10. 验证

hugo --quiet --destination /tmp/build        # 本地构建过不过
curl -o /dev/null -w '%{http_code}' <封面URL>  # 抽样 200
# 再看线上页面的 og:image 是否已换成 .avif

五、六个坑

1. token 不是安全措施,是缓存击穿;但能不用就不用 CDN 对响应的缓存是 immutable、一年。如果某张图在文件还没同步时被访问过一次,404 会被当成结果缓存起来,之后一直 404。所以换图时才加一次性 token 绕开脏缓存;图片本身没变就别加——token 会让 URL 与缓存键一一对应,牺牲跨文章共享缓存(本站封面一对一,损失为零,但换到共用图的场景就不划算)。

2. Pillow 不认识 AVIF Image.open('x.avif') 直接抛 UnidentifiedImageError。校验画质要用 avifdec x.avif x.png 转回 PNG 再比 PSNR。

3. 已存在的文件上传必须带 sha 见上面第 5 步。脚本要幂等就得先 GET 拿 sha。

4. 有的图转完更大 内容简单/已经被强压过的图,换格式可能变大。必须逐张比,大了就跳过。

5. 别用 PSNR 单一指标选参数 我同时看了体积和 dB 两条线:q50 便宜但掉档,q70 画质好但体积回涨。q60 是两条曲线的交叉点。

6. 阈值定太保守等于没做 250KB 只覆盖 47/73,100KB 覆盖全部。先扫一遍分布再定阈值,别拍脑袋。

六、兼容性与回退

格式Safari / iOS 支持起点
WebPSafari 14 / iOS 14(2020)
AVIFSafari 16.4 / iOS 16.4(2023-03)

Android 的 Chromium 内核浏览器(含微信、QQ 浏览器)都没问题;iOS 微信走 WKWebView,系统低于 16.4 就是白图。

我接受这个风险,因为读者主要在国内移动端、且新系统占比高。真要彻底兼底有三条路:

  1. <picture> 三层兜底:AVIF → WebP → JPEG,浏览器自己选(最稳,要改主题模板)
  2. 退回 WebP:把 image: 换成 json 里的 webp 字段,一条命令
  3. 退回原图:换成 ext 拼出来的原始 URL

七、最后的结果

项数值
封面总数73 张
转换前40.1 MB
转换后11.4 MB
节省72%
画质q60,PSNR 37–43dB
原图全部保留,随时可退

一句总结:格式升级的收益 90% 来自"该转的都转了",剩下 10% 才是选哪个格式。先扫描、定阈值、逐张比对体积,这三步做扎实,剩下的只是脚本活。

晚来天欲雪,能饮一杯无