画像を圧縮したら逆に大きくなる原因と対処法
あなたの圧縮ツールは、おそらく壊れていない。画像圧縮がうまくいかないとき、つまり出力が入力と同じサイズで返ってくるか、目に見えて大きくなって返ってくるとき、原因はほぼ必ず三つのうちのどれかだ。そのリストにツールの不具合は入っていない。
よくあるものから片付けよう。ブラウザの canvas 要素を経由して圧縮するツールは、PNG の color type を捨てて、全ピクセルを 32 ビット RGBA として再エンコードする。本サイトの Open Graph カード画像 PNG 83 枚を、Chrome 151.0.0.0 の canvas.toBlob('image/png') に通してみた。83 枚が 83 枚とも、大きくなって返ってきた。増加幅の中央値は +76.6%、最小が +35.6%、最大が +237.2% だった。
二つめの原因は、その JPEG がすでに圧縮済みであること。品質 0.8 で 5 回連続して通すと、ファイルサイズは 2 回目以降まったく動かなくなる。それでいて、どの回も絵は劣化し続ける。
三つめは、その PNG がすでに減色済みであること。2 回目の圧縮が取り除ける色の冗長性は、もう残っていない。
どれも、品質スライダーを下げれば直るものではない。効く手はフォーマットを変えるか、エンコーダを変えるかだ。同じ 83 枚を品質 0.8 の WebP に変換したところ、83 枚すべてが小さくなり、中央値は −94.2% だった。
この数値をどう測ったか。 Playwright で駆動した Chrome 151.0.0.0、macOS 26.5.2、Node v25.8.2、upng-js 2.1.0、ImageMagick。83 枚のセットは本サイトの
public/ogディレクトリにある PNG の全数であり、そこからの抽出標本ではない。加えて、1200×630 と 800×600 で条件を揃えた 4 枚が、写真・グラフィック・パレット・JPEG の各ケースを個別にカバーする。
1. 画像圧縮がうまくいかないとき:30 秒の切り分け表
自分の行を見つけて、そこが指す節を読めばいい。
| 入力したもの | 返ってきたもの | 根本原因 | 読むべき節 |
|---|---|---|---|
| PNG のスクリーンショットやグラフィック | 元より大きい | canvas が 32 ビット RGBA として再エンコードした | 第 2 節 |
| JPEG 写真 | 元より大きい | PNG として保存された | 第 2 節 |
| JPEG 写真 | ほとんど変わらない | すでにサイズの平坦域にいる | 第 3 節 |
| 圧縮ツールを一度通した PNG | 変化なし、またはわずかに増加 | 削れる余地が残っていない | 第 4 節 |
| すべて | 小さくはなったが、ぼやけた・色味がずれた | 世代劣化、または ICC プロファイルの欠落 | 第 3 節と第 6 節 |
各行は排他ではない。canvas ベースの PNG 書き出しに JPEG 写真を放り込めば、最初の二行に同時に当たる。47,828 バイトのファイルが 809,415 バイトになるのは、この経路だ。
診断は飛ばしてとにかく小さいファイルが欲しいなら、画像圧縮ツールを使えばいい。canvas の往復ではなく減色型の PNG エンコーダを動かしていて、結果がアップロードしたファイルより小さくならなかったときは自分の出力のほうを捨てる。
2. 成因その一:ブラウザの canvas は必ず 32 ビット RGBA を書き出す
canvas.toBlob() が PNG に対して実際にやっていること
canvas の往復に、圧縮という工程は存在しない。あるのはデコードと再エンコードで、ブラウザはその間にピクセル値でないものを全部捨てる。
画像を canvas に描画すると、ブラウザはそれをフラットな RGBA バッファへデコードする。1 ピクセルあたり 4 バイト、パレットなし、ビット深度の工夫なし、メタデータなし。続いて HTMLCanvasElement.toBlob() が、そのバッファをゼロからエンコードする。PNG 仕様は六つの color type を定義していて、PNG エンコーダは画像を表現できるもののうち最も安いものを自由に選んでよい。Chrome の canvas エンコーダは選ばない。常に color type 6 を吐く。
| 入力側の color type | canvas.toBlob('image/png') が返すもの |
|---|---|
| RGB(color type 2) | RGBA(color type 6) |
| パレット(color type 3) | RGBA(color type 6) |
| JPEG(PNG の color type を持たない) | RGBA(color type 6) |
これはバイト列から直接読める。PNG ファイルの 25 バイト目がビット深度、26 バイト目が color type で、どちらも IHDR チャンクの中にある。
xxd -s 24 -l 2 -p suspect.png
# 0806 -> ビット深度 8、color type 6(RGBA)
上の三つの入力タイプは、すべて depth=8 type=6 になった。64 色パレットの画像は 1 ピクセルあたり 1 バイトと小さなテーブルで済むが、往復後は 1 ピクセルあたり 4 バイトになり、テーブルは消えている。Deflate がその一部は取り戻すが、全部は戻らない。
手元のブラウザで再現するなら、次のコンソールスニペットを使う。ファイルを選ばせて往復させ、両方のサイズと color type を表示する。
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg';
input.onchange = async () => {
const file = input.files[0];
const bitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
const head = new Uint8Array(await blob.slice(0, 26).arrayBuffer());
console.log(file.name, file.size, '->', blob.size);
console.log('bit depth', head[24], 'color type', head[25]);
};
input.click();
toBlob の品質引数を、ブラウザは image/png では無視する。PNG は可逆なので、品質という数字が引き換えに差し出せるものが何もない。canvas ベースのツールで PNG の圧縮率を操作しているように見えるスライダーは、何も操作していない。
実測 83 枚:圧縮した画像は 83 枚とも元より大きくなった
都合のいい抜き出しはしていない。全数の結果はこうだ。
| 測定項目 | 値 |
|---|---|
| テストしたファイル数 | 83(public/og 内の PNG 全数、RGB とパレット両方の color type を含む) |
| 大きくなったファイル数 | 83 / 83(100%) |
| 増加幅の最小 | +35.6% |
| 増加幅の中央値 | +76.6% |
| 増加幅の最大 | +237.2% |
形が見えるように 1 枚だけ挙げる。aes-decrypt.png は 505,516 B から 898,014 B になり、+77.6% の増加だった。同じファイルを品質 0.8 の WebP でエンコードすると 27,188 B、−94.6% だ。
標本の範囲は正確に押さえておきたい。この 83 枚は Open Graph カードだ。1200×630、平坦な背景、大きな文字、ブランドカラーが数色。優秀な PNG エンコーダが得意とする種類のグラフィックであり、だからこそ canvas の往復がここまで効いてしまう。これはグラフィック型 PNG についての強い結果であって、世界中のあらゆる PNG が canvas の往復で大きくなるという主張ではない。もともとフル RGBA で保存されていた写真型の PNG なら、失うものはずっと少ない。
その一方で、このデータが確かに示していることもある。圧縮後の画像が元より大きく、そのツールがブラウザのタブ内で動いているなら、最初に疑うべきは設定ではなくエンコーダだ。
JPEG を PNG で保存すると 16.9 倍になる理由
データセット全体で最も派手な行がこれだ。条件を揃えた 4 枚を、すべて同じ canvas 経路に通した。
| ファイル | 特徴 | 元サイズ | canvas PNG | 変化 | JPEG q92 | JPEG q80 | WebP q80 |
|---|---|---|---|---|---|---|---|
og-a.png | グラフィック、2,351 色、RGB | 306,302 | 607,481 | +98.3% | 56,459 | 37,695 | 13,852 |
quantized.png | 64 色パレット | 59,843 | 184,856 | +208.9% | 78,388 | 44,442 | 16,046 |
photo.png | 写真、479,373 色 | 498,639 | 867,763 | +74.0% | 77,321 | 45,050 | 24,068 |
photo.jpg | JPEG q82 | 47,828 | 809,415 | +1,592%(16.9×) | 58,762 (+22.9%) | 47,152 | 23,862 |
最後の行は、入力 47,828 バイト、出力 809,415 バイトだ。
この仕組みは知っておいて損がない。「圧縮したら大きくなった」という報告の一群を、まとめて説明できるからだ。JPEG は非可逆の周波数領域コーデックで、8×8 のブロックを DCT 係数に変換し、思い切って量子化し、生き残った係数だけを保存する。PNG は可逆の空間領域コーデックで、各ピクセルを近隣から予測し、残差を deflate で潰す。JPEG をデコードすると、量子化器が持ち込んだアーティファクトをすべて抱えたピクセルが出てくる。エッジ周りにはリンギングが出るし、なめらかなグラデーションにはブロックノイズが乗るし、元画像には無かった微細なノイズも混じる。
そのピクセルを PNG で保存するということは、可逆コーデックに「アーティファクトまで完璧に保存しろ」と頼むことだ。そして可逆コーデックは律儀に応える。ファイルを小さくするために JPEG が作り出したノイズそのものが、今度は PNG を大きくする原因になる。予測型の可逆エンコーダがいちばん苦手とするのがノイズだからだ。
写真の手前に「PNG で保存」がデフォルトとして居座っている場所では、どこでもこれが起きている。スクリーンショットツール、デザインツールの書き出し、チャットクライアント、一部のアップロードウィジェット。
同じピクセル、可逆同士で 3.46 倍の差
canvas のエンコーダは挙動が変わっているうえに、測ってみると単純に弱い。og-a.png のピクセルバッファを、どちらも完全に可逆な二通りの方法でエンコードしてみる。
| エンコーダ | 出力 | color type |
|---|---|---|
Chrome canvas toBlob('image/png') | 607,481 B | RGBA(強制的に格上げ) |
upng-js encode(..., cnum=0) | 175,491 B | RGB(元の color type を維持) |
同一のピクセルで 3.46× の差だ。可逆であることは仮定せずに検証した。cnum=0 の upng-js 出力をデコードすると、入力の RGBA バッファとバイト単位で一致するバッファが返る。この 3.46× と引き換えに差し出したものは、何もない。
これは、よく聞かれるのにまともな答えが返ってこない問いへの回答でもある。どちらも無料で、どちらも同じことを謳っているオンライン圧縮ツールが二つあって、なぜ比較にもならない結果を返すのか。「ブラウザ内で圧縮」という一語が、まったく別の二つの実装を指しているからだ。片方はピクセルを canvas.toBlob に渡し、返ってきたものをそのまま配る。もう片方は本物の PNG エンコーダを積んでいて、color type を自分で決める。同じ入力、同じブラウザで、3.46 倍の開きだ。
3. 成因その二:あなたの JPEG には、もう絞る余地がない
5 回連続で圧縮すると、サイズが動かなくなる
すでに品質 82 で保存されている photo.jpg を、品質 0.8 で 5 回連続して再圧縮する。各世代の出力が、次の世代の入力になる。
| 世代 | バイト | 前世代比 |
|---|---|---|
| 0(元画像、q82) | 47,828 | — |
| 1 | 47,152 | −1.4% |
| 2 | 47,164 | +0.0% |
| 3 | 47,156 | −0.0% |
| 4 | 47,156 | 0.0% |
| 5 | 47,156 | 0.0% |
1 回目で稼げるのは 1.4%。第 2 世代以降、ファイルサイズは ±12 バイトの幅に固定される。第 4 世代と第 5 世代は、サイズが第 3 世代とまったく同じだ。
入力が JPEG のときの「圧縮が効かない」は、こういう見た目をしている。ツールは動いた。エンコーダも動いた。ただ、取り除くものがもう残っていなかった。品質 80 の量子化テーブルは、品質 82 が残していた係数をおおむねすでにゼロにしているからだ。いったん消えた係数を、もう一度消すことはできない。
手元で確かめられる。
cp photo.jpg gen0.jpg
for i in 1 2 3 4 5; do
magick "gen$((i-1)).jpg" -quality 80 "gen$i.jpg"
done
stat -f%z gen0.jpg gen1.jpg gen2.jpg gen3.jpg gen4.jpg gen5.jpg
Linux では代わりに stat -c%s を使う。正確なバイト数は ImageMagick のビルドがどのエンコーダをリンクしているかで変わるので、上の表が桁までそのまま再現されると思わないでほしい。見るべきは形のほうだ。意味のある落ち込みが 1 回あって、あとは平坦。
品質の数値を上げるのは、圧縮ではない
第 2 節の photo.jpg の行をもう一度見てほしい。品質 82 のその元画像を品質 92 で再圧縮すると 58,762 バイトになった。+22.9% だ。
品質パラメータを「小さい」から「大きい」まで好きな位置に置ける単なるつまみだと思っている人は、ここで驚く。これは絶対的な品質の目標値ではなく、量子化テーブルを選ぶスイッチだ。デコードした画像を、それを生み出したテーブルより細かいテーブルに通すと、既存のアーティファクトをより精密に保存したうえで、新しい損失を一段重ねることになる。ファイルは大きく、絵は悪く、その両方が同時に起きる。
ここから出てくる規則はこうだ。JPEG は、保存されたときの品質設定より高い値で再圧縮してはいけない。その値が分からないなら、再圧縮そのものをやめる。元データに戻ればいい。
世代劣化:目には見えない JPEG 再圧縮の画質低下
平坦域の表には罠がある。サイズは第 2 世代以降変わらなくなったが、画像のほうは変わり続けていた。どの回もピクセルへデコードし、再変換し、再量子化する。ある世代で境界ぎりぎりに生き残った係数は、次の世代で境界の向こう側へ押し出される。
損傷は、探している場所には現れない。サムネイルの大きさでは、第 5 世代と第 1 世代は見分けがつかない。100% に拡大して、JPEG がいつも真っ先に崩れる場所を確認してほしい。平坦な背景に接する硬いエッジ、文字、ブロックノイズが 8×8 のタイルとして見えてくるなめらかなグラデーション。デプロイのたびに再圧縮するビルドパイプラインでは、これが何か月も静かに積み上がる。
ここで測ったのはファイルサイズであって知覚品質ではないので、5 世代分の PSNR や SSIM の数値を引くつもりはない。測っていないからだ。それに、示したいことはサイズのデータだけで足りる。第 2 世代からは、コストはすべて画質側に乗り、利益はゼロだ。
色ずれは、きっかけが同じで別種の失敗だ。canvas はメタデータを一切運ばないので、toBlob の往復は EXIF ブロックを落とし、ICC プロファイルも道連れにする。Display P3 や Adobe RGB のタグが付いた画像は、タグの無い状態で出てきて、ビューアはそれを sRGB として解釈する。ピクセル値は動いていない。動いたのは、それをどう解釈するかという指示のほうだ。
4. 成因その三:PNG の容量が減らないのは、すでに減色済みだから
PNG を一度圧縮ツールに通してあるなら、2 回目に手を付ける材料はもう残っていない。これが quantized.png の行だ。すでに 64 色パレットまで減色済みの 59,843 バイトのファイルを、三つの別々の経路に通した。
| 経路 | 結果 | 元サイズ比 |
|---|---|---|
可逆再エンコード(upng cnum=0) | 61,377 | +2.6% |
| 256 色へ減色 | 61,366 | +2.5% |
| 64 色へ減色 | 61,366 | +2.5% |
どの経路も元より大きくなった。差は小さいが、確かに大きい。すでに 64 色だったファイルを 64 色へ減色した場合ですら、そうなる。
PNG の圧縮は、冗長性を取り除くことで成り立つ。繰り返される色、予測できる近隣ピクセル、小さなパレット。前回の圧縮が、それを全部回収済みだ。残っているものはほぼ圧縮不能で、わずかな増加はエンコーダ自身のオーバーヘッドにすぎない。パレットの並び順が前回と少し違ったり、走査線ごとのフィルタ選択が変わったり、deflate の当たりがわずかに悪かったりする、その程度のことだ。
実務上の帰結はこうなる。最適化済みの PNG で「削減 0%」と出るのは、失敗ではなく正しい結果だ。わずかな増加を報告したうえで元ファイルを保持するツールは、正しく振る舞っている。それでも大きいほうのファイルを渡してくるツールは、そうではない。
5. PNG に本当に効くのは減色であって、再エンコードではない
可逆再エンコード、256 色、64 色を比べる
3 枚のファイルと 3 通りの戦略を測った。
| ファイル | 元サイズ | 可逆(cnum=0) | 256 色 | 64 色 |
|---|---|---|---|---|
og-a.png | 306,302 | 175,491(−42.7%) | 110,772(−63.8%) | 76,230(−75.1%) |
photo.png | 498,639 | 587,863(+17.9%) | 109,284(−78.1%) | 61,397(−87.7%) |
quantized.png | 59,843 | 61,377(+2.6%) | 61,366(+2.5%) | 61,366(+2.5%) |
目を引く点が二つある。
可逆再エンコードは、選択肢のなかで最も非力だ。グラフィックでは 42.7% 稼いだが、写真では 17.9% 吐き出し、減色済みのファイルでは 2.6% 損した。写真型のコンテンツは、有能な可逆 PNG エンコーダですら歯が立たない。見つけるべきパレットが無く、隣り合うピクセルが互いをうまく予測できないからだ。
本当の削減は減色のほうにあり、しかもその差は僅差ではない。同じグラフィックで、256 色なら 63.8% 対 42.7%、64 色なら 75.1% だ。コマンドラインではこうする。
magick logo.png -colors 64 PNG8:logo-64.png
magick logo.png -colors 256 PNG8:logo-256.png
PNG8: という接頭辞は、パレット PNG での出力を強制する。これが無いと、ImageMagick は色数を減らしたうえでトゥルーカラーのファイルを書いてしまうことがあり、利得の大半が消える。
パレットが安全なとき、色帯が出るとき
減色は非可逆だ。すべてのピクセルを、限られたパレットのなかで最も近い項目に写像する。だから問題は、そのずれが見えるだけの色数がコンテンツ側にあるかどうかになる。
安全なもの。アイコン、ロゴ、UI のスクリーンショット、フラットなイラスト、図解、そして均一な色の広い面と硬いエッジを持つもの全般。こうした画像に含まれる異なる色は、たいてい多くても数百なので、256 項目のパレットならほぼ無傷で、64 でも耐えることが多い。
危ないもの。写真、なめらかなグラデーション、柔らかいドロップシャドウ、半透明のオーバーレイ。グラデーションを 64 段階に落とせば目に見える色帯が出るし、ディザリングはその色帯をノイズと交換するが、そのノイズが今度はファイルサイズを一部押し戻す。グラデーションの上に乗った部分透明が、いちばん厄介なケースだ。
透明度は個別に確認する価値がある。どれだけ残るかが、減色そのものではなくエンコーダに依存するからだ。ImageMagick の PNG8: は二値の透明度を書くので、ピクセルは完全な不透明か完全な透明のどちらかになり、アンチエイリアスの効いた柔らかい縁は硬くなって返ってくる。専用の PNG 減色エンコーダなら、アルファチャンネルを完全なまま保ち、柔らかい縁も一緒に残す。ドロップシャドウやぼかした縁を持つ素材なら、決める前に両方を比べておきたい。
結果はサムネイルではなく 100% で確認する。縮小プレビューが確実に隠してしまうアーティファクトが、この色帯だ。
6. 対処法:再圧縮ではなく、フォーマットを変える
フォーマット決定表
| コンテンツ | 使うもの | 理由 |
|---|---|---|
| 写真 | WebP、互換性を最優先するなら JPEG | 写真に必要なのは非可逆の周波数符号化 |
| スクリーンショット、UI グラフィック | 減色した PNG、または WebP | 平坦な色、硬いエッジ、小さなパレット |
| アイコンとロゴ | ベクタがあるなら SVG、無ければ減色した PNG | ベクタに解像度の問題は無い |
| 透明度が必要なもの | WebP または PNG | どちらも完全なアルファチャンネルを持つ |
| アニメーション | WebP | GIF の代わりに 1 つのフォーマットで済む |
| ピクセル単位で正確な保存 | PNG、可逆 | 可逆が要件そのものになる唯一のケース |
これは意図的に短くしてある。最近のフォーマット間の符号化効率とブラウザ対応には、WebP と AVIF と JPEG の比較という専用の記事がある。
同じ 83 枚を WebP にしたらどうなったか
canvas の PNG 経路では 100% の確率で大きくなった 83 枚のセットを、代わりに品質 0.8 の WebP へ変換した結果は、83 枚中 83 枚が小さくなり、中央値は −94.2%。先ほどの 1 枚 aes-decrypt.png は、505,516 B から 27,188 B へ、−94.6% だ。
条件を揃えたファイル群も、同じことを言っている。og-a.png は PNG で 306,302、WebP q80 で 13,852。photo.png は PNG で 498,639、WebP q80 で 24,068。photo.jpg は JPEG で 47,828、WebP q80 で 23,862。
最後の比較には但し書きが要る。品質 0.8 の WebP は非可逆なので、PNG と対等な条件で競っているわけではないし、既存の JPEG を WebP へ再エンコードすれば一世代ぶんの劣化は発生する。比べる相手は、仮想的な完璧な元画像ではなく、今このあと出そうとしていたファイルのほうだ。
それでも PNG のままにすべきとき
可逆が、好みではなく要件になる場面もある。デザインの工程に戻して再編集する素材、ピクセル比較テストに使うスクリーンショット、色が 1 つずれるだけでビジュアル差分が壊れる UI の切り出し、そして後から合成されて減色アーティファクトが重なっていくもの。こうしたものは PNG のままにしておく。そのうえで、コンテンツが許すなら本物の減色エンコーダを通し、許さないならファイルサイズを受け入れる。
圧縮とは無関係な、もう一種類の「大きくなった」
画像を data URI としてインライン化する場合、CSS や HTML の中でのサイズは、ディスク上のサイズと一致しない。Base64 は 3 バイトごとに 4 文字を出力し、さらに padding が付くので、そのテキストは運んでいるバイト列より算術的に +33% 大きくなる。転送時の圧縮がかかる前の話だ。完璧に最適化した画像でも、インライン化した瞬間に 3 分の 1 だけ膨らむ。その取引が見合うのはどういうときかは、data URI によるインライン化のガイドで扱っている。
7. 実際にやる:ブラウザ、コマンドライン、ビルドパイプライン
ブラウザで
ブラウザで動くツールを選ぶときに効いてくる違いは、PNG が canvas を通るかどうかだ。画像圧縮ツールでは通らない。PNG の入力はカラーパレットへ減色したうえで、アルファチャンネルを保ったまま本物の PNG として書き戻すので、透明度も柔らかい縁も残る。品質スライダーが動かしているのは、どのみち PNG が無視する toBlob の引数ではなく、パレットのサイズだ。品質 100 なら可逆再エンコードになる。JPEG と WebP のほうは canvas を通り、そこでは品質パラメータが実際に効いて、期待どおりの働きをする。
不具合に見えるが不具合ではない挙動が一つあるので、知っておくといい。圧縮結果がアップロードしたファイルより小さくならなかった場合、ツールは自分の出力のほうを捨てて、元のバイト列を保持する。最適化済みの PNG では「削減 0%」と表示する。これは、第 4 節が意図どおりに働いている姿だ。
処理はローカルで完結する。ファイルはどこにも送らない。
コマンドラインで
cwebp は libwebp に同梱されていて、フォーマットの変更で問題が解けるかどうかを試す最短の手段だ。
# 非可逆 WebP、品質 0-100
cwebp -q 80 photo.png -o photo.webp
# 可逆 WebP、圧縮の手間 0-9
cwebp -z 9 logo.png -o logo.webp
変換と減色のケースは、ImageMagick 7 でまかなえる。
# 指定した品質で PNG を JPEG へ
magick photo.png -quality 80 photo.jpg
# 64 色パレットの PNG へ減色
magick logo.png -colors 64 PNG8:logo-64.png
# EXIF などのメタデータを削除
magick photo.jpg -strip photo-clean.jpg
macOS なら sips が最初から入っていて、依存関係も要らない。
# JPEG へ変換。formatOptions は 0-100 または low/normal/high/best を取る
sips -s format jpeg -s formatOptions 80 photo.png --out photo.jpg
# 長辺が 1200 px になるようリサイズ。アスペクト比は維持される
sips -Z 1200 photo.jpg --out photo-1200.jpg
# 寸法を読み返す
sips -g pixelWidth -g pixelHeight photo-1200.jpg
圧縮の前にリサイズすること。エンコーダが相手にするのはピクセルであり、いちばん安いピクセルは存在しないピクセルだ。
ビルドパイプラインで
手作業から自動化に移った時点で、決めるべきことは「処理をどこでやるか」と「どのライブラリにやらせるか」になり、トレードオフの構図がまるごと変わる。その比較はブラウザ版と Node.js 版の画像圧縮が扱っている。この記事から持ち越す規則は一つだけだ。毎回のビルドで、元のソースファイルから圧縮すること。前回のビルドの出力から圧縮してはいけない。ファイルサイズは完璧に安定して見えるのに、パイプラインが自力で世代劣化の坂を下っていくのは、これをやったときだ。
8. 測定結果が支持しない五つの思い込み
「2 回圧縮すればもっと小さくなる」。第 1 世代で稼げたのは 1.4%。第 2 から第 5 世代は ±12 バイトの範囲にとどまり、そのあいだも絵は劣化し続けた。2 回目はコストしか生まない。
「PNG は可逆だから、より良いフォーマットだ」。可逆は性質であって、美点ではない。テストに使った写真は PNG で 498,639 バイト、WebP q80 で 24,068 バイトだ。画面でしか見られない写真をビット単位で完璧に保つのは、何も買わずにファイルサイズの大半を支払う行為になる。
「品質 100 が安全な選択だ」。品質 82 の JPEG を品質 92 で再圧縮したら +22.9%、しかも画像は悪くなった。元画像の品質設定を超えたところで、その数字は「より安全」を意味しなくなり、「より大きい」を意味しはじめる。
「ファイルが大きいのは解像度が高いからだ」。解像度も効くが、同じ解像度ならフォーマットのほうがもっと効く。og-a.png はどちらでも 1200×630 だ。canvas の PNG なら 607,481 バイト、WebP q80 なら 13,852 バイト。ピクセル数は同一である。
「オンラインの圧縮ツールはどれも同じだ」。同じピクセル、同じブラウザ、どちらも可逆で、canvas からは 607,481 バイト、upng-js からは 175,491 バイト。自分たちを同じように説明している二つのツールのあいだに、3.46 倍の開きがある。
9. FAQ
圧縮した画像が元より大きいのはなぜ?
ツールがそれを圧縮せずに、再エンコードしたからだ。ブラウザの canvas の出力は常に 32 ビット RGBA の PNG で、パレットを捨てて 1 ピクセルあたり 4 バイトを保存する。実際の PNG 83 枚を使ったテストでは 83 枚すべてが大きくなり、増加幅の中央値は +76.6% だった。代わりに WebP か JPEG へ変換するといい。
PNG を圧縮しても容量が減らないのはなぜ?
PNG は可逆なので、品質スライダーには引き換えに差し出せるものが無い。実際の削減は色数を削ることから生まれるが、そのファイルがすでに減色済みなら、削るものは残っていない。テストに使った 64 色の PNG は、2 度目に 64 色へ減色しても +2.5% で返ってきた。
JPEG を 2 回圧縮すると画質は落ちる?
落ちる。しかも見返りはほとんど無い。品質 0.8 で 5 回試すと、47,828 バイトが 1 回目で 47,152 バイトになり、そのあとの 4 回は 12 バイトの幅に固定された。サイズが動かなくなる一方で、どの回も絵を再量子化し続けていた。元データは保管しておくこと。
ファイルを小さくするには PNG と JPEG のどちらを使うべき?
写真なら例外なく JPEG か WebP だ。テストに使った写真は、PNG で 498,639 バイト、JPEG q80 で 45,050 バイト、WebP q80 で 24,068 バイトだった。PNG は、小さなパレットが圧縮の仕事をしてくれる平坦なグラフィック、くっきりした文字、透明度のために取っておく。
圧縮したら画像がぼやけたのはなぜ?
原因は二つある。非可逆エンコーダを低い品質設定で使うと、エッジや文字の周りに目に見えるブロックノイズが出る。繰り返し通した場合は、ファイルサイズが動かなくなったあとも世代劣化が積み上がる。ぼやけたのではなく色がずれたのなら、canvas の往復が ICC プロファイルを落としている。canvas はメタデータをまったく運ばないからだ。
画質を落とさずに画像を圧縮できる?
できる。ただし、効果はずっと小さいと思っておくこと。可逆再エンコードは、同一のピクセルをより効率よく書き直すだけだ。テストのグラフィックでは 306,302 から 175,491 バイトになり、デコード後にバイト単位で一致することを確認した。同じ手を写真に使うと逆方向に振れて、+17.9% だった。目に見える劣化なしで本当に削りたいなら、品質 80 の WebP を使う。
ただのスクリーンショットの PNG が、なぜこんなに大きい?
画面のキャプチャはフル RGBA の PNG として保存され、圧縮前の時点で 1 ピクセルあたり 4 バイトある。さらに Retina ディスプレイなら、縦横それぞれのピクセル数が倍になる。平坦な内容にはパレットの削減がよく効く。テストに使った Open Graph カードのグラフィックは、256 色で 63.8%、64 色で 75.1% 減った。
リサイズは圧縮よりファイルサイズを減らせる?
たいていはそうだし、両者は掛け合わさって効く。縦横を半分にすればエンコーダが動き出す前にピクセルの 4 分の 3 が消えるし、ファイルサイズはおおむねピクセル数に従う。まず実際に表示する寸法までリサイズして、そのあとで一度だけ圧縮する。カメラのフル解像度ファイルをサムネイル枠に放り込めば、どちらの工程も無駄になる。