画像圧縮ベンチマーク: ブラウザで測った10のフィクスチャ
10のフィクスチャ、各5回の測定、1つのブラウザ。サイトが実際に配信するワーカー経路でのエンコーダー測定で、都合の悪い結果も含みます。
何を、どう測ったか?
7つのファイルから得た10ケースの圧縮時間と出力サイズを、1つのブラウザと1台のマシンで各5回測ります。12 MP の写真を JPEG・WebP・AVIF に、2 MP のスクリーンショットを PNG・WebP にエンコードします。
ハーネスは実際に配信される経路を通ります。src/lib/pool.js と src/lib/worker.js を import します。アプリが JPEG・PNG・WebP・AVIF・JXL に使うのと同じプール、同じワーカーです。MotionPhoto では src/lib/motion-photo/pipeline.js を import します。アプリがこの形式で通る経路であり、MotionPhoto も本番ではワーカープールを通りません。ベンチマーク専用の圧縮器も、ずれる可能性のある第二の実装もありません。
各ケースはウォームアップを1回、その後に測定を5回行います。ケースは逐次実行され、4ワーカーのプールを取り合いません。表の時間は5サンプルの中央値です。p95 は最近接順位で計算するため、5サンプルでは最も遅い実行になります。補間せず、5サンプルで裾を語れるふりもしません。
ブラウザはヘッドレス Chrome 155 で、実行ごとに新しい一時プロファイルを使います。拡張機能とバックグラウンド通信は無効です。localhost から配信し、ワーカープールと WASM モジュールの準備ができてから計時を始めます。クロスオリジン分離は意図的に有効にしません。有効にすると wasm-feature-detect がスレッド対応を報告し、oxipng がスレッド版ビルドに切り替わりますが、それは本番の経路ではありません。
マシンは Apple M4、10コア、16 GB メモリ、macOS arm64 です。ブラウザはハードウェアスレッドを10と報告し、アプリはプールを4ワーカーに制限します。
どのファイルを試したか?
0.5 MP の JPEG から 24 MP の写真まで7つのファイル、さらに 12 MP の MotionPhoto 1つです。
| ファイル | ピクセル | 入力 | 何を表すか |
|---|---|---|---|
| photo-12mp.jpg | 4000×3000 | 4.92 MB | 細部の多い写真 |
| shot-2mp.png | 1920×1080 | 1.36 MB | 鋭いエッジと文字風のバーがある UI スクリーンショット |
| flat-1mp.png | 1000×1000 | 118.1 KB | 平坦で低詳細のグラフィック |
| alpha-4mp.png | 2000×2000 | 8.26 MB | 実際のアルファチャンネルを持つ 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 に記録され、ブラウザが読み込む前に検証されます。フィクスチャを変えれば、黙って基準が動くのではなく失敗します。
結果はどうなったか?
10ケースで比率は 1.5% から 69.3%、中央値は 42.0 ms から 1,983.2 ms でした。比率は出力 ÷ 入力で、低いほど小さい。中央値と p95 はワーカー往復を含むケース全体の実時間です。ms/MP は中央値をメガピクセル数で割った値です。
表と2つのグラフは同じ 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 の約3分の1です。12 MP の写真で 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倍です。メガピクセルあたりでは 165.3 ms で、JPEG は 56.4、WebP は 83.1 です。
一行でいえば、JPEG のバイト数の4分の1強でエンコード時間が3倍です。数百枚のバッチではこの時間は現実的で、代表画像1枚なら問題にならないこともあります。
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)で、ここでは3つのコーデックで最速です。メガピクセルあたりのコストはほぼ一定で、速いブロックコーデックらしい姿です。出力は現代の2形式より大きいですが、時間で倒すべき相手は JPEG であり、このマシンでは誰も倒せませんでした。
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 でした。最適化器の固定コストが1メガピクセルにしか分散されないためです。
PNG は可逆です。すべてのピクセルを保ち、oxipng はより小さいストリームのためにフィルターと DEFLATE 戦略を探索します。探索が目的であり、同時にコストです。非可逆コーデックは品質目標に達したら止まれます。可逆最適化器は、これ以上削れるものが無いことを示さなければならず、組み合わせの数は画像とともに増えます。
サイズ差も同じくらい率直です。同じスクリーンショットは PNG で 600.6 KB、WebP で 21.0 KB でした。これは等価な出力ではありません。片方は可逆で片方は非可逆です。しかし、これから編集する図には PNG が正しく、配信する写真には間違っている理由を説明します。
oxipng のレベルを上げると必ず PNG は小さくなるか?
いいえ。レベル3はスクリーンショット3つで 9.6〜13.6% 小さくなりましたが、8.29 MP の写真系 PNG では 8.2倍の時間でわずか4バイトしか減りませんでした。PNG のレベルは bench/png-levels.mjs で別途測りました。同じフィクスチャで、各レベルにつきウォームアップ1回と5回の実行です。3つのスクリーンショット(2.07、8.29、15.73 MP)では、レベル3はレベル1の 7.8〜8.5倍の時間がかかり、ファイルは 9.6〜13.6% 小さくなりました。8.29 MP の写真系 PNG ではレベル1よりわずか4バイト小さく(15,474,918 対 15,474,922)、時間は 8.2倍でした。RGBA 画像では 2.0% 節約し、時間は 7.1倍でした。すでに写真である画像では、追加の探索はほとんど何も買いません。
そのため配信ワーカーは現在、既定でレベル2を使い、4 MP を超えるとレベル1に下げます。16 MP ではレベル2はレベル1より 1.0% 大きく、約2倍遅かったので、高いレベルが自動的に小さいファイルになるわけではありません。変更のコストは平坦で低詳細な PNG に見えます。1 MP の平坦なフィクスチャはレベル2で 62,501 B、レベル3で 48,138 B と 29.8% 大きいものの、エンコードは 437.9 ms で 2,028.3 ms より 4.6倍速い。スクリーンショットの負担は小さく、2.07 MP ではレベル2のファイルはレベル3より 7.2% 大きく、4.1倍速い。サイトは速い既定値を選び、そのバイト数を受け入れました。
MotionPhoto は動画も圧縮するか?
いいえ。再エンコードするのは静止画だけで、動画はバイト単位でコピーされるため、合成 12 MP ファイルは半分を触らないまま 30.7% 縮みました。MotionPhoto は1つのファイルです。JPEG の静止画に動画が付き、機種によっては gain map も付きます。配信経路は静止画をデコードして再エンコードし、その周りにコンテナを再構築します。デコードも再エンコードもせず、出力先としても提示しません。gain map がある場合もコピーし、MPF 内の絶対オフセットを再指定して HDR 表示が壊れないようにします。
つまりファイル全体の比率は、動画がファイルのどれだけを占めるかに支配されます。合成 12 MP フィクスチャでは、静止画は入力 10,463,084 B のうち 5,156,418 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 の経路を実機で検証したもので、合成フィクスチャからの推測ではありません。
限界は何か?
合成フィクスチャと1台のマシンに始まる6つの留保です。
- フィクスチャは ffmpeg が内蔵ソースから生成します。再現可能ですが、カメラの写真ではありません。細部の異なる画像は異なる比率になります。
- MotionPhoto フィクスチャは合成で gain map がありません。ファイル全体の 69.3% は実サンプルの 67.1% ではありません。違いは欠けている gain map と動画の正確なビットレートです。実サンプルは別に報告します。
- この数値は1台のマシン、1つのブラウザ、1つのプールサイズから来ています。別のハードウェアでは絶対時間が変わり、モバイルではさらに変わります。
- 数値は同じ環境内でのみ比較できます。ベンチマーク自身の出力もそう言っています。ローカルの darwin-arm64 と CI の ubuntu-latest は別の runner クラスです。
- これはエンコーダーの測定であり、特定のスマートフォンについての約束ではありません。バッテリー状態、熱スロットリング、ブラウザのバージョン差、結果のデコードコストは含みません。
- ピークメモリは報告しません。測るにはクロスオリジン分離が必要で、それが oxipng の経路を変えます。ベンチマークは別ビルドの数値ではなく "n/a" と書きます。
この数値はどう再現するか?
npm ci から npm run bench までの5つのコマンドです。
npm ci
npm run build
npm run bench:fixtures
npm run bench:build
npm run bench
実行は bench/results/ に JSON を書き、上の表を stdout に出力します。PNG レベルの比較は 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 になりました。
レベル2とレベル3は PNG の出力サイズにどう影響するか?
レベル3のほうが小さいが大幅に遅い。1 MP の平坦な PNG はレベル2で 62,501 B、レベル3で 48,138 B と 29.8% 大きいものの、エンコードは 437.9 ms で 2,028.3 ms より速い。
各ケースは何回測るか?
ウォームアップ1回と測定5回です。表はその5回の中央値を示し、p95 は最も遅いサンプルです。
この記事について
tinyimg が公開しています。ハーネス、フィクスチャ生成器、結果ファイルはリポジトリにあり、この実行の生 JSON は bench/results/ に書かれます。署名は個人ではなくプロジェクトです。ここで主張するのは権威ではなく、方法が書かれていて数値を再現できるという点です。