图片压缩实测:十个样本,在浏览器里跑出来的数字
十个样本,每个测五次,同一台机器、同一个浏览器。数据来自网站实际发布的 worker 路径,也包括那些不太好看的结果。
测了什么,怎么测的?
十个用例来自七个文件,每个测五次:基准测试测量压缩耗时和输出体积。12 MP 照片分别编码为 JPEG、WebP 和 AVIF,2 MP 截图分别编码为 PNG 和 WebP。
测试代码走的是线上实际路径。它直接 import src/lib/pool.js 和 src/lib/worker.js —— 也就是应用处理 JPEG、PNG、WebP、AVIF、JXL 时用的同一个 worker 池和同一个 worker。MotionPhoto 则 import src/lib/motion-photo/pipeline.js,这正是应用处理该格式时走的路径;MotionPhoto 在生产环境同样不经过 worker 池。没有只给基准测试用的压缩器,也没有第二套实现可以悄悄跑偏。
每个用例先跑一次预热,再测五次。用例串行执行,避免争抢四个 worker。表中时间是五次采样的中位数。p95 用最近秩计算,在五个样本下就是最慢的一次 —— 不做插值,也不假装五个样本能刻画长尾。
浏览器是 headless Chrome 155,每次运行使用全新的临时 profile,禁用扩展和后台网络。页面由 localhost 提供,计时从 worker 池和 WASM 模块就绪之后开始。运行时刻意不开启跨源隔离:那会让 wasm-feature-detect 报告支持线程,从而把 oxipng 切到多线程构建,而生产环境用的不是那条路径。
机器是 Apple M4,10 核,16 GB 内存,macOS arm64。浏览器报告 10 个硬件线程;应用的 worker 池上限是四个。
测试了哪些文件?
七个文件,从 0.5 MP 的 JPEG 到 24 MP 的照片,另有一个 12 MP 的 MotionPhoto。
| 文件 | 像素 | 输入 | 代表什么 |
|---|---|---|---|
| photo-12mp.jpg | 4000×3000 | 4.92 MB | 细节丰富的照片 |
| shot-2mp.png | 1920×1080 | 1.36 MB | 带锐利边缘和类文字色条的界面截图 |
| flat-1mp.png | 1000×1000 | 118.1 KB | 平坦、低细节的图形 |
| alpha-4mp.png | 2000×2000 | 8.26 MB | 带真实 Alpha 通道的 RGBA 图像 |
| large-24mp.jpg | 6000×4000 | 9.66 MB | 24 MP 照片 |
| tiny-0.5mp.jpg | 800×600 | 219.6 KB | 小图;单次调用开销在这里更明显 |
| motion-12mp.jpg | 4000×3000 静帧 + 6 秒 1280×720 视频 | 9.98 MB | 用仓库自带写入器合成的 MotionPhoto |
所有样本都由 ffmpeg 从内置素材生成。每个文件的 SHA-256 记录在 bench/fixtures/manifest.json,浏览器加载前会校验;样本被改动会直接失败,而不是悄悄换掉基线。
结果如何?
十个用例的压缩比从 1.5% 到 69.3%,中位数从 42.0 ms 到 1,983.2 ms。比率是输出 ÷ 输入,越低越小。中位数和 p95 是整个用例的墙钟时间,包含 worker 往返。ms/MP 是中位数除以百万像素数。
表格和两张图读取同一个文件 bench/published.json(运行 id、日期、环境);如有出入,以文件为准。
| 用例 | 格式 | MP | 输入 | 输出 | 比率 | 中位数 | p95 | ms/MP |
|---|---|---|---|---|---|---|---|---|
| photo-12mp-jpg | JPEG → JPEG | 12 | 4.92 MB | 444.4 KB | 8.8% | 677.0 | 733.4 | 56.4 |
| photo-12mp-webp | JPEG → WebP | 12 | 4.92 MB | 325.2 KB | 6.5% | 997.5 | 1021.7 | 83.1 |
| photo-12mp-avif | JPEG → AVIF | 12 | 4.92 MB | 121.8 KB | 2.4% | 1983.2 | 2026.8 | 165.3 |
| shot-2mp-png | PNG → PNG | 2.07 | 1.36 MB | 600.6 KB | 43.2% | 613.1 | 618.1 | 296.2 |
| shot-2mp-webp | PNG → WebP | 2.07 | 1.36 MB | 21.0 KB | 1.5% | 151.3 | 156.1 | 73.1 |
| flat-1mp-png | PNG → PNG | 1 | 118.1 KB | 61.0 KB | 51.7% | 516.2 | 562.9 | 516.2 |
| alpha-4mp-avif | PNG → AVIF | 4 | 8.26 MB | 370.1 KB | 4.4% | 1551.7 | 1626.3 | 387.9 |
| large-24mp-jpg | JPEG → JPEG | 24 | 9.66 MB | 846.1 KB | 8.6% | 1228.1 | 1231.0 | 51.2 |
| tiny-0.5mp-webp | JPEG → WebP | 0.48 | 219.6 KB | 20.4 KB | 9.3% | 42.0 | 43.4 | 87.5 |
| motion-12mp | MotionPhoto | 12 | 9.98 MB | 6.92 MB | 69.3% | 1334.0 | 1377.3 | 111.2 |
这些数字说明什么?
在 12 MP 照片上,AVIF 比 JPEG 小 3.65 倍、慢 2.93 倍;WebP 在两者中间,PNG 则是无损的异类,达到 296.2 ms/MP。
AVIF 比 JPEG 小多少?
约为 JPEG 的三分之一:2.4% 对 8.8%,即 121.8 KB 对 444.4 KB。WebP 是 325.2 KB。AVIF 比 JPEG 小 3.65 倍,比 WebP 小 2.67 倍。它也是最慢的:中位数 1,983.2 ms,是 JPEG 的 2.93 倍,是 WebP 的 1.99 倍。按百万像素算,AVIF 是 165.3 ms,JPEG 是 56.4 ms,WebP 是 83.1 ms。
一句话概括这笔交易:用略多于 JPEG 四分之一的体积,换三倍的编码时间。几百张的批量处理里,这个时间很实在;只压一张首图时,可能无所谓。
WebP 比 JPEG 小多少、慢多少?
小 26.8%,慢 1.47 倍:同一张 12 MP 源图上 325.2 KB 对 444.4 KB,997.5 ms 对 677.0 ms。和 AVIF 比,它的体积大 2.67 倍,速度快 1.99 倍。它两头都不占第一,两个方向都停在中间。
JPEG 有多快?
12 MP 照片 677.0 ms(56.4 ms/MP),24 MP 照片 1,228.1 ms(51.2 ms/MP),是这里三种编码器中最快的。每百万像素成本基本持平,这正是快速块编码器该有的样子。它的输出比两种现代格式都大,但比速度它是基准,而在这台机器上没有谁赢过它。
PNG 为什么每百万像素最慢?
2.07 MP 截图上是 296.2 ms/MP,是 WebP 的 73.1 ms/MP 的 4.05 倍,因为它必须为每个像素搜索滤波器和 DEFLATE 策略。这张截图用 PNG 编码耗时 613.1 ms,编码成 WebP 只用 151.3 ms。平坦的 1 MP PNG 更差:516.2 ms,因为优化器的固定成本只摊在一个百万像素上。
PNG 是无损的。每个像素都要保留,oxipng 要花时间搜索滤波器和 DEFLATE 策略,寻找更小的码流;搜索是它的价值,也是它的成本。有损编码器达到质量目标就可以停;无损优化器必须确认没有东西可以再删,而组合数量随图像增大。
体积差距同样直白:同一张截图 PNG 是 600.6 KB,WebP 是 21.0 KB。这两个输出并不等价 —— 一个无损,一个有损 —— 但它们解释了为什么 PNG 适合你还要继续编辑的图表,不适合你要发出去的照片。
oxipng level 越高,PNG 就越小吗?
不一定:level 3 在三个截图样本上小 9.6–13.6%,但在 8.29 MP 的照片型 PNG 上只少四个字节,耗时却是 8.2 倍。PNG level 用 bench/png-levels.mjs 单独测过,样本相同,每个 level 一次预热加五次测量。在三个截图样本上(2.07、8.29、15.73 MP),level 3 的耗时是 level 1 的 7.8–8.5 倍,文件小 9.6–13.6%。在 8.29 MP 的照片型 PNG 上,它只比 level 1 少四个字节 —— 15,474,918 对 15,474,922 —— 耗时却是 8.2 倍。在 RGBA 图像上省了 2.0%,耗时 7.1 倍。图像本身已经是照片型时,多出来的搜索几乎买不到东西。
所以线上 worker 现在默认用 level 2,超过 4 MP 降到 level 1。在 16 MP 时,level 2 比 level 1 大 1.0%,速度还慢约一倍,所以 level 更高并不自动等于文件更小。这个改动的代价在平坦、低细节的 PNG 上看得见:1 MP 平坦样本在 level 2 下是 62,501 B,level 3 是 48,138 B —— 大 29.8% —— 但编码只要 437.9 ms,而不是 2,028.3 ms,快 4.6 倍。截图付出的代价小一些:2.07 MP 时 level 2 比 level 3 大 7.2%,快 4.1 倍。网站选择了更快的默认值,接受这部分体积。
MotionPhoto 会压缩视频吗?
不会:只重新编码静帧,视频按字节原样复制,所以合成的 12 MP 文件小了 30.7%,而其中一半根本没动。一个 MotionPhoto 就是一个文件:一张 JPEG 静帧,附上一段视频,有些机型还带 gain map。线上路径会解码并重新编码静帧,然后围绕它重建容器。视频不解码、不重编码,也不作为输出目标提供。gain map 存在时同样原样复制,并重新指向它在 MPF 中的绝对偏移,HDR 显示才不会坏。
这意味着整文件比率主要取决于视频占多大比例。在合成的 12 MP 样本里,静帧占输入的 5,156,418 B / 10,463,084 B(49.3%),视频占 5,306,666 B(50.7%),没有 gain map。静帧处理后是 1,944,569 B,小了 62.3%;整个文件从 9.98 MB 变成 6.92 MB,小了 30.7%。静帧自身省得很多;整文件省得有限,因为有一半根本没动。
真实样本验证用的是一台 OnePlus 13 的 MotionPhoto:静帧 7,140,978 B(44.7%),视频 7,968,544 B(49.9%),gain map 848,042 B(5.3%),合计 15,957,564 B。走完只压静帧的路径后:静帧 1,862,140 B(小 73.9%),视频和 gain map 字节完全一致,合计 10,712,466 B —— 15.96 MB 变成 10.71 MB,小 32.9%。重建后的文件在手机上被识别为 MotionPhoto,并能正常播放。gain map 这条路径是在真实硬件上验证的,不是从合成样本推断的。
有哪些局限?
六点说明,从合成样本和单台机器开始。
- 样本由 ffmpeg 从内置素材生成。它们可复现,但不是相机拍出来的照片;细节不同的图像会得到不同的比率。
- MotionPhoto 样本是合成的,没有 gain map。它 69.3% 的整文件比率不是真实样本的 67.1%;差异来自缺失的 gain map 和具体视频码率。真实样本单独列出。
- 这些数字来自一台机器、一个浏览器、一个 worker 池大小。换硬件绝对时间会变,移动设备差异更大。
- 数字只在同一环境下可比。基准测试自己的输出也这么说:本地 darwin-arm64 和 CI 的 ubuntu-latest 属于不同的 runner 类别。
- 这些是编码器测量,不是对某款手机的承诺。它们不包含电池状态、热降频、浏览器版本差异,也不包含解码结果的成本。
- 没有报告峰值内存:测量它需要跨源隔离,而那会改变 oxipng 的代码路径。基准测试宁可写 “n/a”,也不拿另一套构建的数字充数。
如何复现这些数字?
五条命令,从 npm ci 到 npm run bench。
npm ci
npm run build
npm run bench:fixtures
npm run bench:build
npm run bench
运行会把 JSON 写进 bench/results/,并把上面的表格打印到 stdout。PNG level 对比用 npm run bench:png-levels。浏览器加载前会用 bench/fixtures/manifest.json 校验样本;文件被改动会直接失败,而不是悄悄生成新基线。
常见问题
哪种格式压缩得最小?
AVIF:在 12 MP 照片上是 121.8 KB,占输入的 2.4%,而 WebP 是 6.5%,JPEG 是 8.8%。
哪种格式编码最快?
JPEG:12 MP 照片用 677.0 ms,WebP 是 997.5 ms,AVIF 是 1,983.2 ms。
PNG 为什么每百万像素更慢?
因为它是无损的:oxipng 要为每个像素搜索滤波器和 DEFLATE 策略。2.07 MP 截图用 PNG 是 296.2 ms/MP,用 WebP 是 73.1 ms/MP,少 4.05 倍。
MotionPhoto 会压缩视频吗?
不会。视频按字节原样复制,只重新编码静帧;合成的 12 MP 样本里静帧小了 62.3%,整个文件小了 30.7%,从 9.98 MB 变成 6.92 MB。
level 2 和 level 3 对 PNG 输出体积有什么影响?
level 3 更小但慢得多。1 MP 平坦 PNG 在 level 2 下是 62,501 B,level 3 是 48,138 B,大 29.8%,但编码只要 437.9 ms,而不是 2,028.3 ms。
每个用例测几次?
一次预热加五次测量;表格报告这五次的中位数,p95 是最慢的一次。
关于本文
由 tinyimg 发布。压测代码、样本生成器和结果文件都在仓库里,这次运行的原始 JSON 写在 bench/results/。署名是项目,不是某个人:这里的依据不是头衔,而是方法写下来了,数字可以复现。