コンテンツにスキップ

レスポンス圧縮

[middleware]
compression = true

たいていのデプロイではこれで全部です。有効にすると、アプリケーションがレンダリングした HTML と JSON が、受け入れるクライアントに対して符号化されます。

既定で無効なのは、アプリケーションの手前で既に圧縮していることが多く、ボディを 2 回 符号化しても誰の得にもならないからです。他に誰もやっていないときに有効にしてください。

このスイッチが効くのは、リクエストを待たせながらアプリケーションがレンダリングするものです。

圧縮する しない
WriteHTML, WriteHTMLPage, WriteHTMLChain WriteProblem
WriteHTMLFragment 静的ファイル。事前圧縮されたサイドカーを自前で持っている
WriteAPI 更新リクエストの拒否。理由は WriteProblem と同じ
await 境界を持つページのストリーミング側
部分更新ライブ領域

すでに Content-Encoding が付いているレスポンスはそのままです。二重に符号化することは ありません。

WriteProblem だけは意図的な例外です。problem ドキュメントは数百バイトを手で組み立てた もので、しかも失敗してはいけない経路にあります。そしてそのサイズでは、どのコーディングでも 結果は元より大きくなる——フレームヘッダだけで、削れる分を上回ってしまう。ここに エンコーダを置くのは、何も得ずに失敗経路をひとつ増やすことです。

同じ理屈は、更新系のレスポンスには例外ではなく下限として効きます。redraw、アクション レスポンス、シーケンスツリーは書き出す前に組み立て終わっているので長さが分かっていて、 512 バイト未満のものはそのまま送ります。ストリーミングするものにこの下限は適用できません ——フレームを開く時点で長さが分からないからです——ので、最初のバイトから符号化します。

部分更新を配信していて、圧縮をアプリケーションの手前に任せているなら、その層が text/html だけでなく application/x-ndjsonapplication/json も対象にしているか 確認してください。ページは圧縮するが差分は圧縮しないプロキシの下では、差分が置き換える はずのページより大きくなり、差分を要求する意味が逆転します。

提供するのは 2 つ。先に試されるのは zstd です。

[middleware]
compression = true
compression_codings = ["zstd", "gzip"]

これが既定値なので、2 行目を書くのは変更したいときだけです。この順序はデプロイのもので あって、クライアントのものではありません。Accept-Encoding の q 値が言っているのは 「何を読めるか」であって、読める 2 つのうちどちらに CPU を使う価値があるかは、クライアントが 下す判断ではない。ただし q=0 は排除として効きます。そちらはたしかに能力についての表明 だからです。

リストから外れたコーディングは、クライアントが要求しても提供されません。つまりこのキーは 順序だけでなく削除も表現します。全部止めたいときは空リストではなく compression = false を使ってください。

自分のトラフィックを測っていないなら、順序は触らないでください。 zstd が先頭なのは、 両者が動くレベルでは比率でわずかに勝っていること、そしてすでに zstd を受け取っている クライアントがそのまま受け取り続けることの 2 点によります。逆にするのは、クライアントが ほぼ gzip で足りるうえに zstd エンコーダをそもそもリンクしたくない場合くらいで、それなら 後述のビルドタグのほうが直接的な答えです。

このリストを持つ価値をつくっているのは gzip のほうです。ブラウザ、プロキシ、クローラ、 コマンドラインクライアント——どれも欠かさない唯一のコーディングだからです。とくに Safari が zstd を広告するのは macOS Tahoe の Safari 26 と iOS の Safari 26.3 からで、しかもその 対応はブラウザではなく OS のネットワークスタック由来です。つまり OS 更新の速度でしか 広がらない。gzip を提供する前、これらのクライアントは元のバイト列を受け取っていました。

brotli はリストにありません。あれは静的アセットのビルドの ものです。リクエストごとに見ると割に合わない——配信できる程度に速いレベルでも zstd の およそ 4 倍の CPU を払ってボディをさらに 8% 削るだけで、純 Go のエンコーダは約 795 KB、 zstd の 3 倍のバイナリを占めます。その zstd を外すためのビルドタグを用意している フレームワークで、です。

Vary: Accept-Encoding は、ボディが実際に符号化されたかどうかに関わらず付きます。これは 見た目以上に重要です。ひとつの表現を保存したキャッシュが、別のものを求めたクライアントに それを渡してはいけない——そう伝えるのがこのヘッダです。

コーディングが選ばれたときは、こうなります。

  • Content-Encoding: zstd または gzip
  • Content-Length は無し。エンコーダが長さを知るのは閉じたあとだからです
  • ETag も無し。ハッシュが読めるのは Close のあとで、その頃にはヘッダはとうに 送られています。検証子が付くのは静的ファイルで、動的にレンダリングされたレスポンスには 付きません

ストリーミングには代償がある

Section titled “ストリーミングには代償がある”

await 境界を持つページは、まずシェルを確定し、各領域を 揃った順に送ります。圧縮もそこに追随し、完了した境界ごとにエンコーダが flush されます。 そうしないと、完了した内容がバッファの中で待たされてブラウザに届きません。

flush はブロックを終わらせ、早く終わったブロックは圧縮率が落ちます。ストリーミングした ページは、同じページをバッファリングした場合より圧縮が効きません。 これがトレードオフで、 たいていは払う価値があります。どちらにせよ最初の描画は早く届くからです。これは両方の コーディングで同じで、どちらが選ばれたかには依りません。

もう少し見えにくい代償もあります。境界ごとの flush は、各領域の圧縮後の長さを個別に 観測可能にします。ページ全体でひとつの長さを見るより細かいオラクルです。攻撃者が制御 できる入力の隣で秘密をレンダリングする境界があるなら、そのページはストリーミングしないで ください。バッファリングでレンダリングします。

[html]
streaming = false

同じキーは、符号化済みレスポンスをバッファリングして段階的配信を台無しにするプロキシへの 回避策でもあります。

符号化するかどうかと、その順序。それ以外はありません。最小サイズも、content-type の リストも、そしてとくにエンコーダのレベルも。短いレスポンスも長いレスポンスと同じように 符号化されます。

レベルを外に出さないのは、それがデプロイによって動かない実測の崖で決まっているからです。 ここでのボディはリクエストを待たせながら符号化されるので、希少な資源はスループットで、 譲るのは比率のほうになります。gzip はレベル 1、zstd は最速設定で走り、どちらもおよそ 450 MB/s で元の 30% 前後に収まります。gzip のレベル 6 なら比率をあと 3 ポイント買えますが スループットは 3 分の 1 に、レベル 9 なら 9 分の 1 になります。深いレベルが失われた わけではありません。アセットのビルドがそれを使います。そちらなら、代償はリクエストに 応答していないマシンに落ちるからです。

他のつまみが必要なのであれば、正直な答えはこうです——アプリケーションの手前にいる リバースプロキシや CDN はすでにそれを持っていて、この仕事にはそちらのほうが向いています。 このスイッチは、そういう層が無いデプロイのためにあります。

使わないときにエンコーダが払わせるもの

Section titled “使わないときにエンコーダが払わせるもの”

compression = false はエンコーダを走らせなくします。コンパイルされること自体は 止まりません。実行時の値がコードをリンクから外すことはできないので、CDN で圧縮を 終端している構成でもバイナリは両方を抱えたままです。

その分が、strip 後で 2つ合わせて 387 KB です。かつてはビルドタグを立てる価値が ありました。zstd を持つことがエンコーダの10倍あるデコーダをリンクすることを意味して いた頃で、Popcorn Web は pw_nozstdpw_nogzip を出してそこから逃げていました。 いまはエンコーダが独立したパッケージになり、デコーダは消え、残った数字は問いより 小さくなっています。両方のタグは削除しました。まだ渡しているビルドが拒否される ことはありません。ただ何も起きないだけです。

なのでスイッチは1つ、設定側だけです。アプリケーションの手前で何かが既に圧縮して いるなら compression = false にして、バイナリはそのままにしてください。