无损压缩

一个 PNG 95KB 的故事

“光影”上线的第三周,一个设计师用户上传了一套 UI 设计稿——全是 PNG 格式的截图和矢量图。

她很快在群里反馈:“我的图上传后变糊了。”

我一看,果然——她上传的是一张 1920×1080 的 UI 截图,PNG 格式,原始大小 95 KB。我的系统检测到 PNG,走的是有损压缩流程,转成 WebP quality=80 后只有 28 KB。

问题是,UI 截图里有大量文字,文字被有损压缩后边缘发虚,肉眼可见。

95 KB 的 PNG → 28 KB 的有损 WebP,看起来省了很多,但 UI 截图的文字糊了。而如果用无损压缩,95 KB 的 PNG 可以转成 58 KB 的无损 WebP——文字完全清晰,体积还小了 39%。

我意识到:不是所有图片都应该有损压缩。

哪些图片应该无损压缩?

先搞清楚哪些类型的图片需要无损压缩:

必须无损的图片类型:
1. UI 截图       → 文字必须清晰,一个像素都不能差
2. 产品白底图    → 白色背景不允许有色块
3. Logo / 图标   → 硬边缘,有损会产生毛刺
4. 文字截图      → 同 UI 截图
5. 线条图/图表   → 硬边缘 + 少量颜色

可以有损的图片类型:
1. 摄影作品      → 噪点天然掩盖压缩失真
2. 风景照        → 渐变丰富,适合有损
3. 人像照        → 肤色过渡,有损压缩效果好
4. 动物/植物     → 自然纹理,失真不明显

关键区别:人眼对”人造内容”(文字、线条、纯色)的失真极度敏感,对”自然内容”(噪点、渐变、纹理)的失真相对迟钝。

PNG 压缩的三个层次

对于需要无损压缩的图片,我研究了 PNG 优化的三个层次:

层次一:标准 PNG 优化(OptiPNG)

PNG 文件内部有很多可以无损去除的冗余数据:不必要的 metadata、非最优的过滤策略、未压缩的文本块等。

结果:

PNG OptiPNG 优化效果(-o5 级别)

图片类型                 原始大小   优化后    节省
─────────────────────────────────────────────────
UI 截图                  95 KB     72 KB    24.2%
白底产品图               320 KB    245 KB   23.4%
Logo 图标                12 KB     8 KB     33.3%
数据图表                 45 KB     32 KB    28.9%
照片(被存为PNG)         2.1 MB    1.8 MB   14.3%

OptiPNG 对所有类型都有 14%~33% 的优化。但这还不够——无损压缩的天花板在哪里?

层次二:有损化 PNG(pngquant)

pngquant 做了一件看似矛盾的事:把 PNG 变成有损的,但仍然是 PNG 格式。 它通过减少颜色数量(从 24 位真彩色降到 8 位调色板)来大幅减小文件体积,同时尽量保持视觉效果。

结果:

PNG pngquant 有损化效果(quality 65-80)

图片类型                 原始大小   有损化后   节省
──────────────────────────────────────────────────
UI 截图                  95 KB     28 KB    70.5%   ← 文字开始轻微发虚!
白底产品图               320 KB    68 KB    78.8%   ← 背景出现轻微色带
Logo 图标                12 KB     4 KB     66.7%   ← 颜色减少,渐变不平滑
数据图表                 45 KB     12 KB    73.3%   ← 细线条变粗
照片(被存为PNG)         2.1 MB    380 KB   81.9%   ← 看起来还行

pngquant 的压缩率非常惊人,但对 UI 截图和 Logo 来说,有损化的代价太大了——文字和线条的质量明显下降。

结论:pngquant 适合”照片被存为 PNG”的情况,不适合真正的 UI 截图和 Logo。

层次三:无损 WebP 转换

这是最终的答案:把 PNG 转成无损 WebP

结果:

PNG → 无损 WebP 转换效果

图片类型                 PNG 大小   无损 WebP   节省     像素差异
──────────────────────────────────────────────────────────────
UI 截图                  95 KB      52 KB     45.3%    0(完全一致)
白底产品图               320 KB     180 KB    43.8%    0(完全一致)
Logo 图标                12 KB      7 KB      41.7%    0(完全一致)
数据图表                 45 KB      24 KB     46.7%    0(完全一致)
照片(被存为PNG)         2.1 MB     1.2 MB    42.9%    0(完全一致)

完美!无损 WebP 在不丢失任何像素信息的前提下,平均减少了 44% 的体积。

三种方法对比

PNG 优化方法对比(UI 截图 95KB 为例)

方法              输出大小   节省    像素损失   适用场景
─────────────────────────────────────────────────────────────
OptiPNG (-o5)     72 KB     24%     无        PNG 格式内优化
pngquant          28 KB     70%     有        照片误存为 PNG 时
无损 WebP         52 KB     45%     无        ✅ 最佳方案
有损 WebP (q=80)  28 KB     70%     有        不适合文字/UI

决策树

图片内容是什么?
├── 照片/自然图像 → 有损 WebP(用上一节的智能压缩)
└── UI/文字/Logo/产品图
    ├── 需要 PNG 格式(兼容性要求)
    │   └── OptiPNG 优化
    └── 可以用 WebP
        └── 无损 WebP ✅ 最佳方案

完整的无损压缩实现

线条图 vs 照片:压缩差异的深层原因

我在测试中发现一个很有意思的现象:同样是 1920×1080 分辨率的图片,线条图的压缩率远低于照片。

结果:

              线条图            风景照
唯一颜色      1,247            482,351
颜色比率      0.06%            23.3%
相邻像素差异   8.7              28.4
信息熵        3.2 bits         16.8 bits

原因

线条图:少量颜色 + 硬边缘 + 低熵
- 颜色少但位置精确 → 需要精确编码每个像素位置
- 硬边缘 → 相邻像素差异大但规律性强
- 低熵 → 无损压缩效果好(Run-Length + 字典编码效率高)
- 但有损压缩会破坏硬边缘的精确性

风景照:海量颜色 + 柔和过渡 + 高熵
- 颜色丰富且连续 → 相邻像素差异小且平滑
- 柔和过渡 → 频域中高频分量少
- 高熵 → 无损压缩效果差(信息量大,很难找到规律)
- 但有损压缩效果极好(可以丢弃大量高频分量)

结论:线条图天生适合无损压缩(信息熵低),照片天生适合有损压缩(高频分量可丢弃)。

前端适配

前端需要知道图片是否需要无损压缩。我在上传时加了一个判断:

后端根据 hint 做最终决定: