JSON 圧縮を間違った順序でやめる:minify vs gzip vs brotli の実測
要点:minify は生の JSON を 30〜50% 削りますが、gzip がその利得の大半を消します。実際の形での実測値と、minify が逆効果になる四つのケースを示します。

要点:まず転送圧縮と構造の刈り込みを分けて考えましょう。gzip や brotli がまだ入っていないなら、サーバー設定を直すのが先です。gzip -6 だけで 94.5% に達し、minify に残る価値は 0.9 ポイントだけです。minify の 46.3% がフルに効くのは、バイトの勘定がローカルに記録される場合だけです。リクエストボディ、Lambda の 6 MB、Firestore の 1 MiB、ディスクやログに書く JSON などです。
お気に入りの minifier の「35% 削減」バッジは正直です。生のバイトを測っているのです。あなたの API は gzip か brotli の上で動き、それらはまったく同じ冗長性を食べます。二つの数値を掛けると誤った答えが出ます。
1. 「圧縮」を二つの別物に分ける
人が JSON 圧縮と呼ぶものは、実は二つの無関係な操作です。それを混ぜることが、あらゆる誤った結論の始まりです。
| minify(空白除去) | gzip / brotli(転送圧縮) | |
|---|---|---|
| 性質 | 文書の変換 | 転送の変換 |
| 出力 | 依然として正しく読める JSON テキスト | バイナリ。デコードするまで使えない |
| 利得が現れる場所 | ディスク、メモリ、ログ、サイズ制限 | 回線とキャッシュのみ |
| デコード後 | バイトは小さいまま | バイトは元のサイズに戻る |
| 誰が担うか | あなたのビルドスクリプト、シリアライザの引数 | Nginx、CDN、サーバーフレームワーク |
重要なのは最後の二行です。プラットフォームの制限はデコード後に測られます。AWS Lambda の 6 MB ペイロード上限、Firestore の 1 文書 1 MiB 上限。gzip を有効にしてもそこでは 1 バイトも得られません。どちらもデコード後の結果を数えるからです。
2. minify が削るのは構造上の空白だけ
RFC 8259 は任意の構造文字の周囲に空白(space、\t、\n、\r)を許します。JSON 文書でそれ以外に省略可能なものはありません。キー名はデータで、数値はすべての桁を保ち、文字列の中身はバイト単位で不可侵です。
ですから操作全体は一行になります。
const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 は構造文字の周囲にしか空白を許さないので、
// 文字列内のスペースはデータであり、整形ではない
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - 内側のスペースは両方とも保たれる
特筆すべきは、「圧縮」の下に分類されるが実際には文書を変更する三つの操作です。値が null のキーの削除、浮動小数点の丸め、キーの並べ替え。これらは文書を再整形するのではなく変更するので、「深い圧縮」を宣伝するツールは、有効にする前に利用者の同意が必要です。
minify は空白しか削らないので、その効果はほぼファイルのどれだけがインデントかに依存します。以下の表で繰り返し現れるはずです。
3. 測定:四つの文書、三つの gzip レベル、そして brotli
環境:Node v22.13.1、zlib.gzipSync のレベル 1 / 6 / 9、brotliCompressSync の既定品質(11)。四つのサンプルは実際に出荷する形を意図的にカバーしています。50 件のネストした注文(深い構造、繰り返しキー)、6 階層の設定(長いキー、短い値)、200 行のログ配列(長い文字列、インデントほぼなし)、このサイトの実物 media-manifest.json(混在した形、URL の多い文字列)。
ステップ 1:生のバイト。
| サンプル | pretty(2 スペース) | pretty(4 スペース) | minify 後 | minify の削減 |
|---|---|---|---|---|
| 50 件のネストした注文 | 29,551 B | 40,761 B | 15,881 B | 46.3% |
| 6 階層の設定 | 25,450 B | 38,012 B | 11,679 B | 54.1% |
| 200 行のログ配列 | 14,582 B | 14,982 B | 13,981 B | 4.1% |
| media-manifest.json | 25,149 B | 28,114 B | 21,222 B | 15.6% |
「35% 削減」バッジはここから来ており、嘘ではありません。問題はステップ 2 に現れます。
ステップ 2:各変種を圧縮し、minify がなおどれだけ足すかを問う。
| サンプル | gzip -1 の上で | gzip -6 の上で | gzip -9 の上で | brotli の上で |
|---|---|---|---|---|
| 50 件のネストした注文 | 21.0% | 17.0% | 8.9% | 10.6% |
| 6 階層の設定 | 74.2% | 59.1% | 58.0% | 56.4% |
| 200 行のログ配列 | -0.7% | -0.4% | 0.6% | -0.3% |
| media-manifest.json | 1.5% | 1.0% | 1.0% | 1.6% |
生のバイトの 46.3% を失う同じネスト注文文書も、gzip -6 が効くとそのうち 17.0% しか残りません。ログ配列は完全にマイナスになります。minify してから圧縮した方が、pretty のまま圧縮するよりわずかに大きいのです。マイナスはバグではなく、まさに予想すべきことで、次の節で理由を説明します。
ステップ 3:元の pretty 文書に対する総削減率。実際に回線を流れる数値です。
| サンプル | gzip -6 のみ | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| 50 件のネストした注文 | 94.5% | 95.4% | 96.9% |
| 6 階層の設定 | 96.0% | 98.4% | 99.0% |
| 200 行のログ配列 | 89.9% | 89.9% | 95.0% |
| media-manifest.json | 83.5% | 83.6% | 86.1% |
4. gzip が minify の功績を食べる理由
gzip は LZ77 の後にハフマン符号化を行います。LZ77 の中核は「32 KB のウィンドウを後方に見て、繰り返しの並びを見つけ、(距離, 長さ) のポインタに置き換える」ことです。インデントはその操作にとって最容易な標的です。各ネスト階層は同一のスペース列を出し、隣接する数十行が同じ組み合わせを繰り返します。ハフマンは高頻度の記号に短い符号を割り当てるので、\n と 8 つのスペースのようなパターン(一つのファイルで千回繰り返されうる)は 2〜3 ビットのコストになります。
ですから minify が削除するバイトは、まさに圧縮器がほぼ無料で畳み込むバイトです。自分でやることは、圧縮器がすでに得意な仕事をやり直すことで、46% は 17% に崩れます。上の表の話はこれで全部です。
マイナスのログ配列の結果には独自の説明が必要です。そのサンプルはほぼすべて長い文字列(ログ行、URL、base64)で、繰り返しが少なくエントロピーが高い。そもそも圧縮器が取り除ける冗長性がほとんどありません。pretty 版の改行がたまたまマッチャーに安定した境界を与えています。それを取り除くと LZ77 はその錨を失います。このサンプルでは、pretty のまま gzip -1 で圧縮すると 1,508 バイト、minify してから圧縮すると 1,518 バイト。10 バイト悪化します。
大きさ順に並べると、レバーはこうなります。
| レバー | 元の pretty 文書に対する削減(ネスト注文) | コスト |
|---|---|---|
| minify | 46.3%(生バイトのみ) | 可読性が失われる。ビルドステップが必要 |
| gzip -6 | 94.5% | サーバー CPU が少し。設定はほぼゼロ |
| minify + gzip -6 | 95.4% | 上の二つを足したもの |
| minify + brotli | 96.9% | HTTPS と brotli モジュールが必要。静的資産は事前圧縮 |
0 から 94.5% への跳躍は gzip の仕事です。94.5% から 95.4% への跳躍、つまり 0.9 ポイントが minify の仕事です。この表が、労力をどこに置くべきかを決めます。
5. 圧縮レベルは隠れた変数
単一の固定 gzip レベルを前提とする議論は砂の上に立っています。レベルが minify の限界価値を直接変えるからです。
| 転送設定 | minify がなお削減する追加バイト(ネスト注文) |
|---|---|
gzip -1(Nginx gzip_comp_level の既定) |
21.0% |
| gzip -6(ほとんどの標準ライブラリの既定) | 17.0% |
| gzip -9 | 8.9% |
| brotli(既定品質) | 10.6% |
転送圧縮が弱いほど、minify の効果は大きくなります。実践的な規則が得られます。gzip_comp_level がまだ 1 の素の Nginx では、minify は 21% をもたらします。9 に調整するか brotli に切り替えると 9〜11% に落ち、通常は省略できます。
どちらかを主張する前に、自分が実際どの段階にいるかを測ってください。
# 同じレスポンスを三つの Accept-Encoding で取得し、届いたものを比較する
for enc in identity gzip br; do
size=$(curl -s -o /dev/null -w '%{size_download}' \
-H "Accept-Encoding: $enc" https://api.example.com/v1/orders)
echo "$enc -> $size bytes"
done
三つの数値が三つの問いに一度に答えます。圧縮は有効か、どのレベルか、minify にどれだけ余地が残るか。identity と gzip がほぼ同じなら、圧縮はまったく有効でなく、直すべきは JSON ではなく Nginx です。
gzip on;
gzip_comp_level 6;
gzip_min_length 256; # 小さなレスポンスは飛ばす。大きくなるだけ
gzip_types application/json application/javascript text/css;
6. minify が本当に効くとき
一つのテストが決めます。削減したバイトはどこかに記録されるか、それとも生バイトを数える制限に当たるか。 はいなら 46% がフルに効きます。「回線にしか存在しない」なら、17% が本当の価値です。
第一に、HTTP リクエストボディ。 ほとんどのサーバーはリクエストボディを解凍せず、gzip はほぼレスポンス専用に設定されます。POST する JSON は通常、転送層の保護がまったくないので、minify の節約は定価で届きます。Webhook コールバック、MQTT テレメトリ、permessage-deflate を交渉しなかった WebSocket フレームも同じです。
第二に、厳格なバイト上限を持つチャネル。 そのすべてがデコード後のバイトを数えます。
| チャネル | 制限 | gzip は助けになるか |
|---|---|---|
| AWS Lambda の同期ペイロード | 6 MB | いいえ。デコード後に測る |
| Firestore の単一文書 | 1 MiB | いいえ |
| Kafka の単一メッセージ(既定) | 1 MB | いいえ。ブローカー側の圧縮は別の層 |
| localStorage のクォータ | 約 5 MB(文字数で数える) | いいえ |
| GET リクエストの URL | サーバー依存。8 KB が一般的 | いいえ |
このカテゴリでは minify は最適化ではなく、リクエストが送信できるか失敗するかの差です。JSON を URL クエリ文字列に埋め込むのが最も際立つケースです。URL は決して圧縮されず、長さ制限は確かにあります。まず minify し、それから base64 が本当に必要かを決めてください。
第三に、ディスクやログに書く JSON。 ログとインデックスは転送層を介さずに生のバイトを保存します。人をつまずかせる注意点があります。minify はもともと pretty JSON を書いていた場合にだけ効きます。その 200 行のログサンプルはわずか 4.1% しか削減しませんでした。行がすでにコンパクトで、インデントがほぼファイルの一部でなかったからです。逆に、ロガーが JSON.stringify(obj, null, 2) を呼んでいるなら、第三引数を外すのは無料の金です。
最後の注記:生バイトが本当に厳しいとき、キー名は通常インデントより大きなレバーです。media-manifest.json で 14 個の一意キーを k0 から k13 に置き換えると、minify 形が 21,222 B から 18,887 B へ、さらに 11.0% 減ります。圧縮すると、その残りの優位は gzip -6 で 2.4%、brotli で 1.3% に縮みます。仕組みは上と同じです。インデントもキー名も繰り返される構造情報で、圧縮器はどちらでも同じ配当を取ります。
7. 逆効果になる四つのケース
コミットするファイルは minify しない。 "qty": 2 を 3 に変えると、pretty ファイルは git diff に一行だけの綺麗な変更を示します。minify ファイルは行全体を書き直し、数百のフィールドでは変更をまったく見つけられません。JSON マージ競合の解決方法で扱ったのと同じ仕組みです。差分ツールは行単位で動くので、一行 JSON はあらゆる編集を「行全体が異なる」に変えます。リポジトリには pretty ファイルを保ち、ビルド出力かストレージへの書き込み時だけ minify してください。
小さなレスポンスを圧縮しない。 {"ok": true} だけのレスポンスはインデントを足すと 16 バイト、gzip -6 後は 36 バイトで、20 バイト大きくなります。gzip 自身の辞書とヘッダーが場所を占めるからです。これは理論ではなく、まさに gzip_min_length が存在する理由です。Nginx の既定 20 バイトは本番には低すぎ、通常は 256 が下限です。
パースが速くなると期待しない。 同じ注文集合を 4,000 レコードに拡大すると、pretty 文書は 2,374,378 バイト、minify 版は 1,282,358 バイトで 46% 小さいのに、JSON.parse の所要時間の中央値は 5.7 ms から 5.0 ms へ、およそ 12% しか速くなりません。サイズを半分にしても時間は半分になりません。パーサーは予算の大半を文字列の切り出しと数値の変換に費やし、空白の読み飛ばしは付随的なものだからです。パースコストを実際に決めるのはキー数と文字列の中身で、大きな JSON ファイルのパース、描画、ストリーミングが詳しく分解しています。
すでに brotli や gzip -9 で動いているサービスに後付けしない。 利得は 9〜11% に落ち、コスト(デバッグ時の可読性の恒久的な喪失)は残ります。代わりに gzip_types が application/json をカバーしているか確認してください。多くのサイトは gzip を有効にしても text/html しか圧縮せず、JSON はまったく圧縮されないままです。その状況で最初の一歩は minify ではなく、設定の修正です。
8. 判断表:どの工程を先に
| あなたの状況 | 最初にやること | minify の順位 |
|---|---|---|
| 転送圧縮がまったくない | Nginx の gzip_types を設定するか、CDN の自動圧縮を有効にする |
省略。利得の九割が蒸発する |
| gzip -1 で動いている(Nginx 既定) | gzip_comp_level を 6 に上げる |
二番目。21% の価値 |
| すでに brotli か gzip -9 | 長いキーを監査し、ページネーションかフィールド削減を加える | 省略。9〜11% しか残らない |
| Lambda 6 MB や Firestore 1 MiB に当たっている | 直接 minify する。46% がフルに効く | 一番目 |
| POST ボディ、Webhook、MQTT | 直接 minify する。転送圧縮はここではほとんど効かない | 一番目 |
| Git に入る設定ファイル | pretty のまま保つ | ビルド出力のみ |
| ログ、テレメトリ、オフラインダンプ | ロガーの null, 2 引数を外す |
それが最初の一歩 |
一文の規則:どのバイト勘定を縮めようとしているかを見ましょう。ローカルに記録されるもの(ディスク、クォータ、プラットフォーム制限、リクエストボディ)か、回線にしか存在しないものか。 ローカル優先なら minify、回線のみならまず brotli か gzip を有効にします。
実際には、上の curl ループを実行して転送圧縮が生きていることを確認し、サンプルを JSON Minify ツールに通して削減率を読みます。その数値が 40% を超え、curl が identity と gzip をほぼ同じサイズで報告するなら、ボトルネックは JSON ではなくサーバー設定です。二つの curl 数値がすでに近ければ、この文書から得られるものはほとんど残っておらず、代わりにフィールドの削減かページネーションを行うべきです。JSON API 契約設計の要点の主題です。
minify した JSON はまだ正しい JSON ですか?
はい。minify は RFC 8259 が構造文字の周囲に許す空白だけを削除し、文字列の中身には触れないので、JSON.parse は元と同じ値を生成します。犠牲になるのは人間の可読性だけです。
brotli か gzip か?
同じ minify サンプルで、brotli は 96.9%、gzip -6 は 95.4% を削減し、差はわずかです。動的レスポンスには CPU を抑えるため gzip -6 を使い、brotli は HTTPS で配信する静的資産に取っておき、.br ファイルに事前圧縮します。
JSON を minify したのに API が速くなりません。なぜですか?
curl -sI でレスポンスに Content-Encoding: gzip ヘッダーがあるか確認してください。多くの Nginx 設定は gzip_types を text/html だけに既定し、JSON はまったくカバーされません。46% のうち五分の一も回線に届いていなかったのです。
設定ファイルは minify してコミットすべきですか?
いいえ。差分ツールは行単位で比較するので、一行 JSON はあらゆる編集を「行全体が変わった」に変え、競合の解決は節約したバイトよりはるかに高くつきます。リポジトリには pretty 版を保ち、ビルド時に minify してください。
gzip が割に合うにはレスポンスがどれくらいの大きさであるべきですか?
通常のしきい値は 256 バイトから 1 KB です。{"ok": true} のような小さなレスポンスは、インデント付きで 16 バイト、gzip -6 で 36 バイトに増えます。辞書とヘッダーのコストが回収分を上回るからです。それがまさに gzip_min_length が制御するものです。