设计师发来一叠 WebP,接收端只认位图老牌格式,只能转 PNG。转完尺寸没变、透明还在,唯独文件从几百字节变成几万,最狠的一张 358 B 变 28946 B,涨了 80.8547 倍。我以为是工具把图搞坏了,比过像素才知道,坏掉的是我对「无损」的理解:PNG 承诺像素不改,从不承诺字节数。
先看 13 组实测:1.4194 倍到 80.8547 倍
同一批素材逐张跑过 WebP 转 PNG,挑 4 张覆盖不同类型:
| 样本 | 输入 | 输出 | 倍数 |
|---|---|---|---|
flat_ui_1440_lossless.webp | 358 B | 28946 B | 80.8547× |
gradient_256_q80.webp | 826 B | 64408 B | 77.9758× |
lossy_photo_400_q80.webp | 1996 B | 107853 B | 54.0346× |
one_px.webp | 62 B | 88 B | 1.4194× |
1×1 的 one_px 只多 26 B 容器开销,1440×900 纯色 UI 里,无损 WebP 花 358 B 记下 6 种颜色,交给 RGBA 容器要 28946 B。批量 8 张的汇总条原样是「已转换 8 / 8 张 · 合计 17.5 KB → 499.8 KB(体积增加 2764%)」,来自输入 17872 B 与输出 511807 B 的字节加权 28.6373 倍,不是单张倍数的平均。
涨的是编码方式,不是画质
5 组源解码与输出解码的对照里 diffPixels = 0、maxChannelDelta = 0、totalAbsDelta = 0,颜色数一字不差:23511 → 23511、6 → 6。涨的只能是描述像素的方式:输出固定 colour type 6 的 RGBA,chunk 只有 IHDR、IDAT、IEND,没有 pHYs 和 tRNS,也不建调色板,每像素 4 字节——1440×900 摊开 5184000 字节原始 RGBA,压完仍要 28946 B。
反差和直觉相反:无损 WebP 涨得最温和,12938 B → 43232 B 只有 3.3415 倍;q80 照片爆得最狠,1996 B → 107853 B 是 54.0346 倍;同一张文字截图,无损 15.4036 倍、q80 只有 4.6521 倍。WebP 先把高频扔掉、码流短到几百字节,PNG 却照样按每像素 4 字节重铺。diffPixels = 0 也只保证不新增损失:转完颜色数仍是 28504,色带被原样钉死,第 234 行只有一次 drawImage,没有抖动。
图 1|条形按输出字节除以输入字节:同样叫涨了几倍,纯色 UI、渐变和有损照片根本不是一个量级。
什么时候确实必须转
- 接收端解不了 WebP。转出的是标准 IHDR、IDAT、IEND 的 PNG,13/13 张宽高等于输入尺寸,1×1 到 1600×1200 共 12 种尺寸零缩放。
- 透明还要在下游被编辑,「保留透明通道」强制勾选并禁用,48635 个全透明、4641 个半透明、36724 个不透明像素一字不差进 PNG;同一张转 JPG 是 9633 B,透明全被 #ffffff 填掉。
- 要把图当算法输入逐像素取样:interlace 0、RGBA 每像素 4 字节,读回来的值与 WebP 解码 diffPixels = 0。
- 只想让网页变小就别转,13/13 张全部变大,最小 1.4194 倍;真要省字节只能反向,PNG 转 WebP 那侧才有质量滑块。
怎么把涨幅压住
先划掉一条路:PNG 页签下质量整行 display=none,toBlob 收到 quality 是 undefined,滑块拖到 10 只改显示文本,重转字节数不变;也没有体积上限和压缩开关,静态扫描 MAX、slice(0, N) 命中 0 次,10718 B 的 large_1600_q80 照转成 255958 B。
能动的是内容和容器:同一批像素换编码器再编一次,RGBA 与参考输出是 64408 对 571、28946 对 5524、4459 对 1421、50000 对 39338。
图 2|深浅两条同一把尺;差距来自编码器不建调色板,不是可调项。
64408 对 571 差 112.8 倍,涨出来的大半是 RGBA 平铺冗余,换容器比换参数有效;50000 对 39338 只差 1.27 倍,说明大头是真信息,压不动。必须交付 PNG 的,转完再走一道只动过滤器的 PNG 压缩;接收端没说不能收 WebP 的,别绕这一圈。
几个静默发生的坑
- 动图只出首帧:animated_2frame.webp 338 B → 1045 B、同素材 GIF 597 B → 1028 B,都是 200×120 单帧,chunk 里无 acTL,帧率与循环全丢。
- alpha = 0 的像素 RGB 被清零:输入 (255,0,0,0) 输出 (0,0,0,0),57600 个像素颜色数 1,580 B 涨到 2072 B。
- 没有格式白名单,accept 只写 image/*,39587 B 的 JPEG 数据起名 fake_webp_is_jpeg.webp 照样解成 1200×900 出 504277 B;只有 notes.txt 触发「已跳过非图片文件:notes.txt」。
- 打包依赖第 57 行 cdnjs 的 jszip 3.10.1:联网时 8 张得 512759 B 的 ZIP,比成员总和 511807 B 多 952 B;拉不到脚本时退化成只下第 1 张 88 B 的 one_px.png,没有报错。
Q:涨这么多是不是转坏了?
不是。diffPixels = 0、maxChannelDelta = 0,颜色数一张没变,13/13 张 IHDR 宽高也与输入相等。涨的全在编码:每像素 4 字节、不建调色板,1.4194 倍和 80.8547 倍是同一条规则的两端。
Q:能让工具少涨一点吗?
不能。质量行被隐藏、quality 传 undefined,拖到 10 输出不变;也没有体积上限和「变大就退回原格式」的分支,第 224 到 236 行只有 drawImage 加 toBlob。能拧的只有转前定格式、转后补一道压缩。
Q:这次到底该转还是该压?
先看是接收端不支持 WebP,还是体积不满意。前者必须转,13 组全部变大也要认;后者转了只会更糟。拿不准就按内容估:纯色 UI 80.8547×、q80 照片 54.0346×、无损照片 3.3415×。
这批素材最后照转了,只是我把 17872 B 变 511807 B、增加 2764% 那一行贴进需求单。真正改掉的是协作顺序:交付格式在开工前由接收端定,而不是转完才发现装不进去。