看不见的 trade-off

一个让我纠结的数字

小李上传了一张 6000×4000 的照片,浏览器只需要显示 800px 宽。

800×533,看起来很简单。但质量参数设多少?

结果:

quality=50:  18 KB   ← 色块明显,像油画
quality=60:  24 KB   ← 细节模糊
quality=70:  34 KB   ← 可接受,但放大能看出差异
quality=75:  42 KB   ← 边缘略有柔和
quality=80:  52 KB   ← 肉眼几乎看不出和原图的区别 ← 我选了这个
quality=85:  68 KB   ← 和 80 几乎一样
quality=90:  92 KB   ← 多出的 40KB 几乎无意义
quality=95:  138 KB  ← 浪费存储

quality=80 是甜蜜点。但”肉眼几乎看不出”是主观判断——我需要一个客观指标。

用 SSIM 量化质量差异

SSIM(结构相似性)是衡量两张图片相似度的标准指标,范围 0~1,1 表示完全相同。

结果:

quality=50: SSIM=0.8521, size=18 KB   ← 差异明显
quality=60: SSIM=0.8934, size=24 KB   ← 可感知差异
quality=70: SSIM=0.9312, size=34 KB   ← 轻微差异
quality=75: SSIM=0.9523, size=42 KB   ← 接近阈值
quality=80: SSIM=0.9678, size=52 KB   ← 高质量 ← 最佳平衡
quality=85: SSIM=0.9789, size=68 KB   ← 边际收益递减
quality=90: SSIM=0.9876, size=92 KB   ← 几乎无损
quality=95: SSIM=0.9934, size=138 KB  ← 浪费

质量-体积曲线有一个拐点:quality=80 之后,文件大小增长很快,但 SSIM 提升很慢。

          SSIM
         1.00 ┤
              │                        · quality=95
         0.99 ┤                    ·
              │                ·
         0.98 ┤            ·  quality=90
              │        ·
         0.97 ┤    · ← quality=80 (拐点)
              │  ·
         0.96 ┤ ·
              │·
         0.95 ┤ quality=75
              │
         0.90 ┤
              │
         0.85 ┤ quality=50
              └───┬────┬────┬────┬────┬──→ size
                 20   50   70  100  140 KB

不同场景的最佳参数

基于 SSIM 分析和业务需求,我制定了不同场景的质量策略:

场景尺寸格式质量依据
列表页缩略图200pxWebP70扫一眼就走,体积优先
卡片封面400pxWebP75首屏要快,SSIM 仍在 0.95 以上
详情页主图800pxWebP80SSIM 0.9678,肉眼几乎看不出差别
原图浏览2000pxWebP82会放大看细节,留一点余量
头像200pxJPEG85圆形裁剪的边缘对压缩敏感
UI 截图原尺寸无损 WebP100文字必须清晰,走无损流程

质量参数不是越高越好。前面那张表已经说明:质量参数从 80 提到 95,SSIM 只从 0.9678 涨到 0.9934,文件大小却从 52 KB 涨到 138 KB——多出来的 86 KB 换来的这点 SSIM 提升,在真实浏览场景里几乎感知不到。

真正要做的是把”参数表”和”场景”绑定在一起,让业务方按名字取参数,而不是自己写数字:

# 场景 -> 派生规格。这是唯一的事实来源,其他地方一律从这里取
SCENE_SPECS = {
    "list_thumb":    {"width": 200,  "format": "webp", "quality": 70, "lossless": False},
    "card_cover":    {"width": 400,  "format": "webp", "quality": 75, "lossless": False},
    "detail_main":   {"width": 800,  "format": "webp", "quality": 80, "lossless": False},
    "full_view":     {"width": 2000, "format": "webp", "quality": 82, "lossless": False},
    "avatar":        {"width": 200,  "format": "jpeg", "quality": 85, "lossless": False},
    "ui_screenshot": {"width": None, "format": "webp", "quality": 100, "lossless": True},
}


def spec_for(scene: str) -> dict:
    """按场景取派生规格。场景名写错要立刻报错,而不是悄悄用默认值。"""
    if scene not in SCENE_SPECS:
        raise KeyError(f"未知的场景: {scene}")
    return SCENE_SPECS[scene]

这张表有三个设计上的讲究:

  1. UI 截图走的是无损流程。这里的 quality: 100 加 lossless: True 不是”高质量有损”,而是无损编码。前一节讲过,UI 截图的有损化会让文字发虚,这种图根本不该进有损压缩流水线。
  2. 头像的质量参数比主图还高。因为头像会被裁成圆形再缩小一圈,裁剪边缘的压缩伪影在小尺寸下特别明显。
  3. 场景名是业务语言,不是技术语言。前端要的是”卡片封面”,不是”400 宽、q75 的 WebP”。场景名由业务定义,参数由系统维护,参数变了不需要改业务代码。

最后一条纪律:这张表要跟着标注集一起维护。每隔一段时间用新的标注图片重跑一遍 SSIM 扫描,如果某个场景的最佳质量参数已经漂移了,就更新这张表。参数表和实验数据脱节,是压缩策略退化的最常见原因。

分辨率与设备适配

不同设备的屏幕分辨率差异巨大:

最佳实践:按 逻辑像素 × 2 来生成图片——在绝大多数设备上够清晰,体积也不会太大。

我的思考

思考 1

如果你发现 quality=75 和 quality=80 的图片在文件大小上差了 10KB,但 SSIM 只差 0.015。在什么情况下应该选择 75?什么情况下选择 80?

图片压缩

解题思路:

关键不是 75 和 80 谁「更好」,而是先判断图片处在什么使用场景:用户是「扫一眼就走」还是「仔细看」。场景定了,质量取舍就定了;再叠加流量量级和是否付费,就能落到具体数值。

详细步骤:

选 75 的场景:

  1. 列表页/缩略图:用户快速滑动浏览,不会停留看细节。10KB × 20 张图 = 200KB 的差异,直接影响首屏加载速度。

  2. 移动端弱网环境:3G 网络下 200KB 需要约 0.3 秒加载。节省 200KB = 快 0.3 秒。

  3. 高流量页面:首页每天 100 万 PV,每张图节省 10KB = 每天节省 10GB 流量 = 每月节省 2,400 元 CDN 成本。

选 80 的场景:

  1. 详情页/全屏查看:用户会仔细看图片内容,质量差异会被注意到。

  2. 付费内容/专业用户:摄影师的作品展示,质量是核心价值。

  3. 低流量页面:用户相册页每天只有 1000 PV,10KB 的差异微不足道。

决策框架:

是"扫一眼就走"还是"仔细看"?
├─ 扫一眼(列表/卡片) → quality=70~75
└─ 仔细看(详情/全屏) → quality=80~85

用户付费了吗?
├─ 免费/广告支持 → 可以适当降低质量节省成本
└─ 付费/专业用户 → 质量优先,不惜多花存储和带宽

流量级别?
├─ 高流量(日 PV > 10 万) → 每KB都值得优化
└─ 低流量(日 PV < 1 万) → 质量优先

最终答案:

看场景选:列表页 / 缩略图、移动弱网、高流量页面用 75;详情页全屏、付费或专业展示、低流量页面用 80。判断顺序是——先看用户会不会细看,再看是否付费,最后看流量级别;两者差距只有 10KB、SSIM 只差 0.015 时,这个量级的取舍完全由场景决定。

发现内容有问题?

告诉我们哪里需要改进。