Webページの読み込みが速いサイトほど、実は裏側で「送る前にデータを小さく圧縮している」ことが多い。HTMLやCSS、JavaScriptは、サーバーが送信する直前にgzipやBrotliという形式で圧縮され、ブラウザ側で解凍されてから画面に表示される。この圧縮・解凍のやり取りは画面には一切現れないが、レスポンスの転送量を大きく左右する仕組みである。前回・前々回に扱ったOGPやHTTPキャッシュヘッダーと同じく、<head>ではなくHTTPレスポンスヘッダーに現れる話として、今回はこの圧縮の仕組みを整理する。
補足: gzip は1992年に登場した古くからある圧縮形式で、対応していないブラウザ・サーバーがほぼ存在しないほど普及している。Brotli は2015年にGoogleが公開した比較的新しい形式で、同じ内容でもgzipより高い圧縮率になりやすいという特徴を持つ。両者は「同じ問題(データを小さくする)に対する別の実装」という関係で、現在の主要ブラウザはどちらも解凍に対応している。
圧縮は何をしているのか — 繰り返しを短い符号に置き換える
gzip も Brotli も、内部ではDEFLATE(gzipの場合)に代表される圧縮アルゴリズムの考え方をベースにしている。ごく単純化すると、やっていることは2段階である。
- 同じ文字列の繰り返しを、「前に出てきた場所への参照」に置き換える(辞書的な置き換え)
- 出現頻度の高い文字・パターンほど短いビット列に、低いものほど長いビット列に割り当てる(ハフマン符号化に代表される考え方)
HTMLやCSS、JavaScriptのようなテキストは、タグ名・関数名・インデントの空白などの繰り返しが非常に多いため、この2段階の置き換えだけでファイルサイズが大きく縮む。逆に、すでにランダムに近い状態のデータ(後述する画像・動画などの既圧縮データ)は繰り返しが少なく、圧縮してもほとんど縮まない。
Brotli はこの基本的な考え方に加えて、Web上でよく使われる単語やHTML/CSSの定型句をあらかじめ内蔵辞書として持っている点が gzip と異なる。この内蔵辞書のぶん、特にWeb向けコンテンツでは gzip より高い圧縮率になりやすい。
サーバーとブラウザの「合意」— Accept-Encoding と Content-Encoding
圧縮された内容を送っても、受け取る側が解凍できなければ意味がない。この食い違いを防ぐため、ブラウザとサーバーはリクエストのたびに次の2つのヘッダーで対応形式を確認し合っている。
| ヘッダー | 送信者 | 役割 |
|---|---|---|
Accept-Encoding |
ブラウザ→サーバー | 「自分が解凍できる形式」を列挙して伝える(例: gzip, deflate, br) |
Content-Encoding |
サーバー→ブラウザ | 実際に使った圧縮形式を伝える(例: br) |
サーバーは Accept-Encoding の中から自分が対応している形式を選び、その形式で圧縮したレスポンスを返す。どちらの形式を使うかを最終的に決めているのはサーバー側であり、ブラウザは候補を提示するだけという役割分担になっている。
実際に wpmm.jp/blog に対して Accept-Encoding を変えてリクエストを送ると、この選択の様子がそのまま観察できる。
# gzip と br 両方を提示 → br が選ばれる
curl -s -D - -o /dev/null -H "Accept-Encoding: gzip, br" https://wpmm.jp/blog/ \
| grep -i content-encoding
# content-encoding: br
# gzip だけを提示 → gzip が選ばれる
curl -s -D - -o /dev/null -H "Accept-Encoding: gzip" https://wpmm.jp/blog/ \
| grep -i content-encoding
# content-encoding: gzip
# 圧縮なし(identity)を指定 → Content-Encoding ヘッダーなし(無圧縮のまま)
curl -s -D - -o /dev/null -H "Accept-Encoding: identity" https://wpmm.jp/blog/ \
| grep -i content-encoding
# (出力なし)
このブログは Xserver 上で動いており、配信経路のWebサーバー(nginx)が Accept-Encoding に応じて Brotli・gzip・無圧縮を動的に選び分けていることが、このやり取りから確認できる。
Vary: Accept-Encoding を忘れると起きること
サーバーとブラウザの間にCDNやリバースプロキシのキャッシュ層が挟まっている場合、もう1つ重要なヘッダーがある。Vary: Accept-Encoding である。
これは「同じURLでも Accept-Encoding の値が違えばキャッシュも別扱いにしてほしい」というキャッシュ層への指示である。これが抜けていると、最初に圧縮なしでアクセスしたクライアントの無圧縮レスポンスがキャッシュされ、後から来た(gzip対応の)別のクライアントにもそのまま無圧縮のレスポンスが配られてしまう、あるいはその逆に、圧縮に対応していないクライアントへ圧縮済みレスポンスがそのまま渡ってしまい文字化けのような表示崩れを起こす、といった事故につながる。前回のOGPの回で触れた「クローラ側のキャッシュが古い状態を引きずる」問題と根は同じで、キャッシュ層は「同じURLなら同じ内容」を前提にしているため、内容が変わりうる軸(言語・圧縮形式など)は明示的に申告しておく必要がある、という考え方はここでも共通している。
自社実装にある「同じ仕組み」— ZIPファイルもDEFLATEを使っている
HTTPの圧縮というと縁遠く感じるかもしれないが、実はこのアプリ自身のコードの中にも、同じ系統の圧縮アルゴリズムを使っている箇所がある。設定・サイトデータをまとめてダウンロードするバックアップエクスポート機能(site_manager_web.py::backup_export)は、Pythonの zipfile モジュールでZIPファイルを生成する際に次のように圧縮方式を指定している。
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
# sites_*.json / settings.json / server_profiles.json をまとめて格納
...
zipfile.ZIP_DEFLATED は、ZIPファイルの中身を DEFLATE アルゴリズムで圧縮するという指定である。gzip はこの DEFLATE を土台にヘッダーとチェックサムを付け足した形式であり、HTTPレスポンスをgzipで圧縮する処理と、ZIPファイルの中身を圧縮する処理は、コンテナ(包み方)が違うだけで中身のアルゴリズムはほぼ同じという関係になっている。用途は全く別でも、「繰り返しを短い符号に置き換えて縮める」という土台は共通している。
圧縮しても意味がないもの
圧縮はどんなデータにも有効というわけではない。すでに圧縮されているデータに追加で圧縮をかけても、ほとんど縮まないばかりか、圧縮処理自体のCPUコストだけがかかって損をすることがある。
- JPEG・PNG・WebP等の画像: 画像フォーマット自体が既に独自の圧縮を内包している
- MP4等の動画: 同様に既圧縮
- 既にgzip/zip化されたファイル: 二重圧縮しても縮み代がほとんど残っていない
このため、Webサーバーの圧縮設定は通常、HTML・CSS・JavaScript・JSON・SVGのようなテキスト系のMIMEタイプだけを対象にし、画像・動画・既圧縮ファイルは対象から除外するのが定石になっている。
動的圧縮と事前圧縮のトレードオフ
圧縮のかけ方にはもう1つ論点がある。リクエストが来るたびにその場で圧縮する(動的圧縮)か、あらかじめ圧縮済みのファイルを用意しておいて配信時はそれを返すだけにする(事前圧縮)か、という選択である。
Brotli には圧縮の強さ(quality)を指定するオプションがあり、強くかけるほど圧縮率は上がるがCPU時間も増える。アクセスのたびに動的圧縮する構成では、レスポンス速度を優先して控えめな圧縮強度を使うことが多い。一方、変更頻度の低い静的ファイル(CSS・JSの配布ファイルなど)は、デプロイ時に一度だけ最大強度で圧縮しておき、配信時は圧縮済みファイルをそのまま返すだけにする構成が可能で、この場合は圧縮そのもののコストを配信の都度払わずに済む。HTTPキャッシュヘッダーの記事で扱った「一度作った結果を使い回す」という発想は、ここでも同じ形で効いてくる。
よくある落とし穴
- 既圧縮フォーマット(画像・動画)まで圧縮対象にしてしまう: 縮まないのにCPUだけ消費する
Vary: Accept-Encodingを忘れる: キャッシュ層が圧縮形式の違いを無視し、誤った形式のレスポンスを配ってしまう- gzipとBrotliの圧縮レベルの数値を混同する: gzipは1〜9段階、Brotliは0〜11段階と、数値の意味も範囲もアルゴリズムごとに異なる
- HTTPSでない配信ではBrotliが選ばれないことがある: 主要ブラウザはセキュリティ上の理由からHTTP接続ではBrotliの
Accept-Encoding候補を送らない実装になっていることが多く、gzipのみになる
まとめ
gzipとBrotliは、どちらも「繰り返しを短い符号に置き換える」という同じ土台の上に成り立つHTTPレスポンスの圧縮形式であり、Brotliは内蔵辞書のぶんWeb向けコンテンツで有利になりやすい。どちらを使うかは、ブラウザがAccept-Encodingで提示した候補の中からサーバーが選ぶという役割分担で決まり、Vary: Accept-Encodingを添えることでキャッシュ層との食い違いも防げる。既に圧縮されたデータには圧縮をかけない、動的圧縮と事前圧縮のコストを使い分ける、といった判断が、実務でのチューニングの軸になる。