四张截图 955 KB,转 WebP 后 105 KB:为什么纯色截图能压到 6%
同一批界面截图,PNG 合计 955 KB,WebP 后只有 105 KB。压缩率不是靠降质换来的——纯色占比高的截图在 WebP 里几乎可以无损压到原来的二十分之一。附具体参数和实测对比。
Lighthouse 报了一条:Improve image delivery — Est savings of 537 KiB。
那是我首页「真实案例」区的四张界面截图。全部 1200×777,PNG 格式。加起来 955 KB,占整个页面传输量的三分之二。
转成 WebP 之后:105 KB。省了 850 KB。
先说结论
| 文件 | PNG | WebP | 比例 |
|---|---|---|---|
| shot-tool.png | 283 KB | 17 KB | 6% |
| shot-shop.png | 195 KB | 34 KB | 17% |
| shot-shop-en.png | 184 KB | 33 KB | 18% |
| shot-tool-en.png | 291 KB | 20 KB | 7% |
| 合计 | 955 KB | 105 KB | 11% |
压缩率最高的是 6%,最低 18%。差异来自画面内容,下面解释。
为什么能压到 6%
这不是「有损压缩堆出来的」。看这几张图的实际构成:
- 深色背景(
#08090b)占 70% 以上 - 文字是纯白和几个灰阶
- 没有照片,没有渐变噪声
大面积同色 + 有限的色阶,正是 WebP 最擅长的场景。
PNG 的压缩(DEFLATE)是逐行无损压缩,对「大片相同像素」的效率远不如 WebP 的块内预测。一张 70% 是同一个颜色的图,PNG 仍要为每个像素存索引,WebP 直接记录「这一块都是这个颜色」。
对照一下:如果截图里有照片、复杂渐变、或大量抗锯齿边缘,压缩率会掉到 40–60%。6% 是纯色截图的特殊待遇,不是普遍规律。
具体参数
from PIL import Image
import os
for name in ["shot-shop.png", "shot-tool.png", "shot-shop-en.png", "shot-tool-en.png"]:
im = Image.open(name).convert("RGB")
out = name.replace(".png", ".webp")
im.save(out, "WEBP", quality=82, method=6)
o, n = os.path.getsize(name), os.path.getsize(out)
print(f"{name:22s} {o//1024:>4} KB → {n//1024:>4} KB ({n*100//o}%)")
两个参数值得说:
quality=82 — 不是随便选的。界面截图的失真在 80–85 之间基本看不出来(文字边缘轻微柔化),低于 75 文字开始发糊,高于 90 体积增长明显但肉眼看不出差别。
method=6 — 编码耗时换体积。PIL 的 method 范围 0–6,6 最慢但压缩率最高。对四张图来说多花不到 1 秒,值得。
如果图多到几百张,用 method=4 平衡时间。
验证压缩没伤到画面
体积小不代表画面对。我用像素级对比确认了一遍:
import numpy as np
from PIL import Image
a = np.array(Image.open("shot-tool.png").convert("RGB")).astype(int)
b = np.array(Image.open("shot-tool.webp").convert("RGB")).astype(int)
diff = np.abs(a - b)
print("最大通道差:", diff.max())
print("平均差:", diff.mean())
输出是最大差 14、平均差 2.14(满值 255)。最大差 14 出现在文字抗锯齿边缘,肉眼不可辨。
这一步不能省。 有一次我压一张带细线条的图,体积掉了 80%,但线条出现了振铃伪影——只看体积会以为成功了。
换掉引用,别只换文件
生成 WebP 之后,页面里的 src 要跟着改:
sed -i 's|/images/shot-tool\.png|/images/shot-tool.webp|g' app/**/page.tsx
漏改的话浏览器继续请求旧 PNG,WebP 白压。
我犯过一次:文件转了,src 忘了改,Lighthouse 复测分数一点没动,白折腾半小时。
Next.js Image 的一个陷阱
如果你用 <Image>,注意 unoptimized 这个属性:
<Image src="/images/shot-tool.webp" width={1200} height={777} unoptimized />
加上 unoptimized,Next.js 会原样输出,不做任何处理——不转格式、不按 sizes 生成多档、不加哈希。
什么时候该加:
- 图片已经在构建前优化过(就是本例)
- 静态导出(
output: 'export')且没有图片服务
什么时候不该加:
- 你希望 Next.js 自动按设备像素比生成多档尺寸
- 源图是用户上传的、尺寸不可控
本例我保留了 unoptimized,因为 WebP 已经压到极限,Next 再压一遍收益很小,反而多一层缓存键。
顺带处理掉的图标
同一轮里还有三个图标偏大:
| 文件 | 之前 | 之后 | 做法 |
|---|---|---|---|
| favicon.ico | 20.6 KB | 2.8 KB | 只保留 16/32 两档 |
| apple-touch-icon.png | 39 KB | 14 KB | 180×180,量化 128 色 |
| logo-192.png | 44 KB | 15.8 KB | 同上 |
favicon.ico 的 20 KB 是因为里面塞了 16/32/48/64 四个尺寸。实际用到的主要是 16 和 32(浏览器标签页),48/64 只有少数场景。砍掉后体积降到 2.8 KB:
src = Image.open("logo-512.png").convert("RGBA")
src.save("favicon.ico", format="ICO", sizes=[(16,16), (32,32)])
注意 .convert("RGBA") 之前踩过坑——如果原图是 RGB 转 ICO,Next.js 构建会报 The PNG is not in RGBA format!。
最终结果
| 指标 | 之前 | 之后 |
|---|---|---|
| 四张截图 | 955 KB | 105 KB |
| 整页传输 | ~1.5 MB | 424 KiB |
| Lighthouse 图片项 | 省 537 KiB 建议 | 通过 |
| 性能分 | 93 | 97 |
没有降质,没有改布局,只是换了个容器格式。
什么图不该这么干
不是所有图都适合:
| 类型 | 建议 |
|---|---|
| 界面截图、图表、纯色插画 | WebP 收益巨大(本例) |
| 照片、复杂渐变 | WebP 收益 40–60%,仍值得 |
| 需要透明通道 + 无损 | PNG 或 WebP lossless,不要用 quality |
| 图标、简单几何 | SVG,体积小一个数量级 |
| 需要被社交平台抓取的 OG 图 | 继续用 JPG/PNG,WebP 兼容性还不完整 |
最后这条容易忽略:og:image 用 WebP 时,部分平台的抓取器还是读不到。分享卡片会变成空白。
一句话总结
先看图的构成,再选格式。 纯色截图转 WebP 能压到 10% 以下,照片只能压到一半——但两者都比直接丢 PNG 强。