Varyヘッダーを理解する

公開: 2022-03-10
簡単な要約↬VaryHTTPヘッダーは、毎日数十億のHTTP応答で送信されます。 しかし、その使用は当初のビジョンを実現したことはなく、多くの開発者はそれが何をするのかを誤解しているか、Webサーバーがそれを送信していることにさえ気づいていません。 クライアントのヒント、バリアント、およびキーの仕様の登場により、さまざまな応答が新たなスタートを切りました。

Vary HTTPヘッダーは、毎日数十億のHTTP応答で送信されます。 しかし、その使用は当初のビジョンを実現したことはなく、多くの開発者はそれが何をするのかを誤解しているか、Webサーバーがそれを送信していることにさえ気づいていません。 クライアントのヒント、バリアント、およびキーの仕様の登場により、さまざまな応答が新たなスタートを切りました。

何が変わるの?

Varyのストーリーは、Webがどのように機能するかについての美しいアイデアから始まります。 原則として、URLはWebページではなく、銀行取引明細書などの概念的なリソースを表します。 銀行の明細書を見たいと想像してみてくださいbank.comにアクセスして、 /statementGETリクエストを送信します。 これまでのところ良いですが、ステートメントをどの形式にするかを指定していません。これが、ブラウザのリクエストにAccept: text/htmlなども含まれる理由です。 理論的には、少なくとも、これは、代わりにAccept: text/csvと言って、同じリソースを異なる形式で取得できることを意味します。

ユーザーと銀行の間でコンテンツ交渉された会話のイラスト
(拡大版を表示)

同じURLがAcceptヘッダーの値に基づいて異なる応答を生成するようになったため、この応答を格納するキャッシュは、そのヘッダーが重要であることを認識する必要があります。 サーバーは、 Acceptヘッダーが次のように重要であることを通知します。

 Vary: Accept

これは、「この応答は、リクエストのAcceptヘッダーの値によって異なります」と読むことができます。

これは基本的に今日のWebでは機能しません。 いわゆる「コンテントネゴシエーション」は素晴らしいアイデアでしたが、失敗しました。 ただし、これはVaryが役に立たないという意味ではありません。 Webでアクセスするページのかなりの部分には、応答にVaryヘッダーが含まれています。おそらく、Webサイトにもそれらがあり、あなたはそれを知りません。 それで、ヘッダーがコンテンツネゴシエーションで機能しない場合、なぜそれがまだそれほど人気が​​あるのか​​、そしてブラウザーはそれをどのように処理するのでしょうか? 見てみましょう。

以前、コンテンツ配信ネットワーク(CDN)、サーバーとユーザーの間に配置できる中間キャッシュ(Fastly、CloudFront、Akamaiなど)に関連してVaryについて説明しました。 ブラウザもVaryルールを理解して応答する必要があり、これを行う方法は、VaryがCDNによって処理される方法とは異なります。 この投稿では、ブラウザのキャッシュバリエーションの曖昧な世界を探ります。

ジャンプした後もっと! 以下を読み続けてください↓

ブラウザで変化する今日のユースケース

前に見たように、Varyの従来の使用法は、 AcceptAccept-Language 、およびAccept-Encodingヘッダーを使用してコンテンツネゴシエーションを実行することであり、歴史的に、これらの最初の2つは惨めに失敗しました。 Accept-Encodingを変更して、GzipまたはBrotliで圧縮された応答を提供します。サポートされている場合、ほとんどの場合、かなりうまく機能しますが、最近ではすべてのブラウザーがGzipをサポートしているため、それほどエキサイティングではありません。

これらのシナリオのいくつかはどうですか?

  • ユーザーの画面の正確な幅の画像を提供したいと考えています。 ユーザーがブラウザのサイズを変更すると、新しい画像がダウンロードされます(クライアントのヒントによって異なります)。
  • ユーザーがログアウトする場合、ログイン中にキャッシュされたページを使用しないようにします(CookieをKeyとして使用します)。
  • WebP画像形式をサポートするブラウザのユーザーは、WebP画像を取得する必要があります。 それ以外の場合は、JPEGを取得する必要があります。
  • 高密度画面でブラウザを使用する場合、ユーザーは2倍の画像を取得する必要があります。 ブラウザウィンドウを標準密度の画面に移動して更新すると、1倍の画像が表示されます。

ずっとキャッシュします

すべてのユーザーが共有する1つの巨大なキャッシュとして機能するエッジキャッシュとは異なり、ブラウザは1人のユーザー専用ですが、明確で特定の用途のためにさまざまなキャッシュがあります。

ブラウザのキャッシュの図
(拡大版を表示)

これらのいくつかは非常に新しく、コンテンツがロードされているキャッシュを正確に理解することは複雑な計算であり、開発者ツールでは十分にサポートされていません。 これらのキャッシュの機能は次のとおりです。

  • 画像キャッシュ
    これは、デコードされた画像データを格納するページスコープのキャッシュであるため、たとえば、同じ画像をページに複数回含める場合、ブラウザはそれを1回ダウンロードしてデコードするだけで済みます。
  • プリロードキャッシュ
    これもページスコープであり、リソースが通常キャッシュできない場合でも、 Linkヘッダーまたは<link rel="preload">タグにプリロードされたものをすべて格納します。 画像キャッシュと同様に、ユーザーがページから移動すると、プリロードキャッシュは破棄されます。
  • サービスワーカーキャッシュAPI
    これにより、キャッシュバックエンドにプログラム可能なインターフェイスが提供されます。 したがって、Service WorkerのJavaScriptコードを介して特に配置しない限り、ここには何も格納されません。 また、 fetchハンドラーで明示的に行う場合にのみチェックされます。 Service Workerキャッシュはオリジンスコープであり、永続的であるとは限りませんが、ブラウザーのHTTPキャッシュよりも永続的です。
  • HTTPキャッシュ
    これは、人々が最もよく知っているメインキャッシュです。 これは、 Cache-ControlなどのHTTPレベルのキャッシュヘッダーに注意を払う唯一のキャッシュであり、これらをブラウザ独自のヒューリスティックルールと組み合わせて、何かをキャッシュするかどうか、およびその期間を決定します。 それは最も広い範囲を持ち、すべてのWebサイトで共有されています。 したがって、2つの無関係なWebサイトが同じアセット(たとえば、Google Analytics)をロードする場合、それらは同じキャッシュヒットを共有する可能性があります。
  • HTTP / 2プッシュキャッシュ(または「H2プッシュキャッシュ」)
    これは接続とともに存在し、サーバーからプッシュされたが、接続を使用しているページからまだ要求されていないオブジェクトを格納します。 特定の接続を使用するページにスコープが設定されます。これは、基本的に単一のオリジンにスコープされるのと同じですが、接続が閉じると破棄されます。

これらのうち、HTTPキャッシュとServiceWorkerキャッシュが最適に定義されています。 画像キャッシュとプリロードキャッシュに関しては、一部のブラウザは特定のナビゲーションのレンダリングに関連付けられた単一の「メモリキャッシュ」としてそれらを実装する場合がありますが、ここで説明するメンタルモデルはプロセスについて考える正しい方法です。 興味がある場合は、 preloadに関する仕様書を参照してください。 H2サーバープッシュの場合、このキャッシュの運命についての議論は活発なままです。

リクエストがネットワークに出かける前にこれらのキャッシュをチェックする順序は重要です。何かをリクエストすると、キャッシュの外側のレイヤーから内側のレイヤーにプルされる可能性があるためです。 たとえば、HTTP / 2サーバーがスタイルシートを必要とするページと一緒にプッシュし、そのページがスタイルシートに<link rel="preload">タグをプリロードする場合、スタイルシートは3つに接触することになります。ブラウザのキャッシュ。 まず、H2プッシュキャッシュに配置され、要求されるのを待ちます。 ブラウザがページをレンダリングしてpreloadタグに到達すると、HTTPキャッシュ(スタイルシートのCache-Controlヘッダーによっては保存される可能性があります)を介して、プッシュキャッシュからスタイルシートを引き出し、保存します。プリロードキャッシュにあります。

ブラウザキャッシュを介したHTTP / 2PUSHフロー
(拡大版を表示)

バリデーターとしてのVaryの紹介

では、この状況を考慮してVaryをミックスに追加するとどうなりますか?

中間キャッシュ(CDNなど)とは異なり、ブラウザは通常、URLごとに複数のバリエーションを保存する機能を実装していません。 これの理論的根拠は、私たちが通常Varyに使用するもの(主にAccept-EncodingAccept-Language )は、単一のユーザーのコンテキスト内で頻繁に変更されないということです。 Accept-Encodingは、ブラウザのアップグレード時に変更される可能性があります(ただし、おそらく変更されません) Accept-Languageは、オペレーティングシステムの言語ロケール設定を編集した場合にのみ変更される可能性があります。 また、この方法でVaryを実装する方がはるかに簡単ですが、一部の仕様作成者はこれが間違いであると信じています。

ブラウザが1つのバリエーションのみを保存することはほとんどの場合大きな損失ではありませんが、「変化した」データが変更された場合に、無効になったバリエーションを誤って使用しないことが重要です。

妥協案は、 Varyをキーではなくバリデーターとして扱うことです。 ブラウザは通常の方法で(基本的にはURLを使用して)キャッシュキーを計算し、ヒットを記録した場合、リクエストがキャッシュされた応答に組み込まれているVaryルールを満たしていることを確認します。 そうでない場合、ブラウザはリクエストをキャッシュのミスとして扱い、キャッシュの次のレイヤーに移動するか、ネットワークに送信します。 新しい応答を受信すると、技術的には異なるバリエーションであっても、キャッシュされたバージョンが上書きされます。

さまざまな行動を示す

Varyの処理方法を示すために、小さなテストスイートを作成しました。 このテストでは、ヘッダーごとに異なるさまざまなURLをロードし、リクエストがキャッシュにヒットしたかどうかを検出します。 私はもともとこれにResourceTimingを使用していましたが、互換性を高めるために、リクエストが完了するまでにかかる時間を測定することに切り替えました(そして、違いを明確にするために、サーバー側の応答に意図的に1秒の遅延を追加しました)。

各キャッシュタイプと、 Varyがどのように機能するか、そして実際にそのように機能するかどうかを見てみましょう。 ここでは、テストごとに、キャッシュからの結果(「HIT」と「MISS」)を期待する必要があるかどうか、および実際に何が起こったかを示します。

プリロード

プリロードは現在Chromeでのみサポートされており、プリロードされた応答はページで必要になるまでメモリキャッシュに保存されます。 応答は、HTTPキャッシュ可能である場合、プリロードキャッシュに向かう途中でHTTPキャッシュにも入力されます。 プリロードでリクエストヘッダーを指定することは不可能であり、プリロードキャッシュはページの間だけ持続するため、これをテストするのは困難ですが、少なくともVaryヘッダーを持つオブジェクトが正常にプリロードされることがわかります。

GoogleChromeでのリンクrel = preloadのテスト結果
(拡大版を表示)

Service Worker Cache API

ChromeとFirefoxはサービスワーカーをサポートしており、サービスワーカーの仕様を開発する際に、作成者はブラウザーの実装が壊れていると見なしたものを修正して、ブラウザーのVaryをCDNのように機能させることを望んでいました。 つまり、ブラウザはHTTPキャッシュに1つのバリエーションのみを保存する必要がありますが、キャッシュAPIには複数のバリエーションを保持することになっています。 Firefox(54)はこれを正しく実行しますが、ChromeはHTTPキャッシュに使用するのと同じバリデーターとしての変更ロジックを使用します(バグは追跡されています)。

GoogleChromeでのServiceWorkerキャッシュのテスト結果
(拡大版を表示)

HTTPキャッシュ

メインのHTTPキャッシュはVaryを監視する必要があり、すべてのブラウザーで一貫して(バリデーターとして)監視します。 これについては、MarkNottinghamの投稿「StateofBrowser Caching、Revisited」を参照してください。

HTTP / 2プッシュキャッシュ

Varyすることを確認する必要がありますが、実際にはそれを尊重するブラウザーはなく、ブラウザーはプッシュされた応答を、応答が変化するヘッダーにランダムな値を持つ要求とうまく一致させて消費します。

GoogleChromeでのH2プッシュキャッシュのテスト結果
(拡大版を表示)

「304(未修正)」のしわ

HTTPの「304(未変更)」応答ステータスは魅力的です。 私たちの「親愛なるリーダー」であるArturBergmanは、HTTPキャッシング仕様(私の強調)でこの宝石を私に指摘しました。

304応答生成するサーバーは、同じ要求への200(OK)応答で送信される次のヘッダーフィールドのいずれかを生成する必要があります: Cache-ControlContent-LocationDateETagExpires 、およびVary

304応答がVaryヘッダーを返すのはなぜですか? これらのヘッダーを含む304応答を受信したときに実行することになっていることについて読むと、プロットが厚くなります。

保存された応答が更新用に選択された場合、キャッシュは、保存された応答の対応するヘッダーフィールドのすべてのインスタンスを置き換えるために、304(未変更)応答で提供される他のヘッダーフィールドを使用する必要があります。

待って、何? したがって、 304Varyヘッダーが既存のキャッシュされたオブジェクトのヘッダーと異なる場合、キャッシュされたオブジェクトを更新することになっていますか? しかし、それは私たちが行った要求とはもはや一致しないことを意味するかもしれません!

そのシナリオでは、一見すると、 304は、キャッシュされたバージョンを使用できることと使用できないことを同時に通知しているように見えます。 もちろん、サーバーが実際にキャッシュされたバージョンを使用することを望まない場合は、 304ではなく200を送信します。 したがって、キャッシュされたバージョンを確実に使用する必要がありますが、更新を適用した後は、最初に実際にキャッシュに入力したものと同じ将来のリクエストに再度使用されない可能性があります。

(補足:Fastlyでは、この仕様の癖を尊重していません。したがって、オリジンサーバーから304を受け取った場合、TTLをリセットする以外は、キャッシュされたオブジェクトを変更せずに使用し続けます。)

ブラウザはこれを尊重しているように見えますが、癖があります。 それらは、応答ヘッダーだけでなく、それらとペアになる要求ヘッダーを更新して、更新後に、キャッシュされた応答が現在の要求と一致することを保証します。 これは理にかなっているようです。 仕様にはこれが記載されていないため、ブラウザベンダーは自由に好きなことを行うことができます。 幸いなことに、すべてのブラウザがこれと同じ動作を示します。

クライアントのヒント

Googleのクライアントヒント機能は、長い間ブラウザでVaryに発生する最も重要な新しいことの1つです。 Accept-EncodingAccept-Languageとは異なり、クライアントヒントは、ユーザーがWebサイト内を移動するときに定期的に変化する可能性のある値、具体的には次のように記述します。

  • DPR
    デバイスのピクセル比、画面のピクセル密度(ユーザーが複数の画面を使用している場合は異なる場合があります)
  • Save-Data
    ユーザーがデータ保存モードを有効にしているかどうか
  • Viewport-Width
    現在のビューポートのピクセル幅
  • Width
    物理ピクセル単位の必要なリソース幅

これらの値は、1人のユーザーで変更される可能性があるだけでなく、幅に関連する値の範囲が広くなります。 したがって、これらのヘッダーでVaryを完全に使用できますが、キャッシュの効率が低下したり、キャッシュが無効になるリスクがあります。

キーヘッダーの提案

クライアントヒントやその他の非常にきめ細かいヘッダーは、Markが取り組んできたKeyという名前の提案に役立ちます。 いくつかの例を見てみましょう。

 Key: Viewport-Width;div=50

これは、応答がViewport-Widthリクエストヘッダーの値に基づいて変化するが、50ピクセルの最も近い倍数に切り捨てられることを示しています。

 Key: cookie;param=sessionAuth;param=flags

このヘッダーを応答に追加するということは、 sessionAuthflagsという2つの特定のCookieを変更していることを意味します。 変更されていない場合は、この応答を将来のリクエストに再利用できます。

したがって、 KeyVaryの主な違いは次のとおりです。

  • Keyを使用すると、ヘッダー内のサブフィールドを変更できます。これにより、1つのCookieのみを変更できるため、突然Cookieを変更できるようになります。これは膨大な量になります。
  • 個々の値を範囲にバケット化して、キャッシュヒットの可能性を高めることができます。これは、ビューポートの幅などを変更する場合に特に便利です。
  • 同じURLを持つすべてのバリエーションは、同じキーを持っている必要があります。 したがって、キャッシュがすでにいくつかの既存のバリアントを持っているURLの新しい応答を受信し、新しい応答のKeyヘッダー値がそれらの既存のバリアントの値と一致しない場合、すべてのバリアントをキャッシュから削除する必要があります。

執筆時点では、ブラウザやCDNはKeyをサポートしていませんが、一部のCDNでは、着信ヘッダーを複数のプライベートヘッダーに分割し、それらを変えることで同じ効果を得ることができる場合があります(「さまざまなものを最大限に活用する」の投稿を参照してください。迅速に」)、したがって、ブラウザはKeyが影響を与えることができる主要な領域です。

すべてのバリエーションが同じキーレシピを持つための要件はやや制限されており、仕様にある種の「早期終了」オプションが必要です。 これにより、「認証状態によって異なり、ログインしている場合は設定によっても異なります」などの操作を実行できます。

バリアント提案

Keyは優れた汎用メカニズムですが、一部のヘッダーには値に関するより複雑なルールがあり、それらの値のセマンティクスを理解すると、キャッシュの変動を減らす自動化された方法を見つけるのに役立ちます。 たとえば、2つのリクエストに異なるAccept-Language値、 en-gben-usが含まれているとしますが、Webサイトでは言語のバリエーションがサポートされていますが、「英語」は1つしかありません。 米国英語のリクエストに応答し、その応答がCDNにキャッシュされている場合、 Accept-Languageの値が異なり、キャッシュが十分にスマートではないため、英国英語のリクエストに再利用することはできません。 。

かなりのファンファーレで、Variantsの提案を入力してください。 これにより、サーバーはサポートするバリアントを記述できるようになり、キャッシュはどのバリエーションが実際に区別され、どのバリエーションが事実上同じであるかをよりスマートに判断できるようになります。

現在、Variantsは非常に初期のドラフトであり、 Accept-EncodingAccept-Languageを支援するように設計されているため、その有用性はブラウザキャッシュではなく、CDNなどの共有キャッシュに限定されています。 しかし、それはKeyとうまくペアになり、キャッシュの変動をより適切に制御するための全体像を完成させます。

結論

ここで取り入れるべきことはたくさんあります。ブラウザが内部でどのように機能するかを理解することは興味深いかもしれませんが、ブラウザから抽出できる簡単なこともいくつかあります。

  • ほとんどのブラウザは、 Varyをバリデーターとして扱います。 複数の個別のバリエーションをキャッシュする場合は、代わりに異なるURLを使用する方法を見つけてください。
  • ブラウザは、HTTP / 2サーバープッシュを使用してプッシュされたリソースのVaryを無視するため、プッシュするものを変更しないでください。
  • ブラウザには大量のキャッシュがあり、さまざまな方法で機能します。 特にVaryのコンテキストでは、キャッシュの決定がそれぞれのパフォーマンスにどのように影響するかを理解することをお勧めします。
  • Varyはそれができるほど有用ではなく、ClientHintsとペアになっているKeyはそれを変え始めています。 ブラウザのサポートに従って、いつ使用を開始できるかを確認してください。

進んで、変化しなさい。