Understanding the Vary header in the browser
*以下の記事は、Smashing Magazine に [Andrew が寄稿した記事](https://www.smashingmagazine.com/2017/11/understanding-vary-header/)を編集したものです。*
私は以前、[CDN における Vary の活用方法](https://www.fastly.com/blog/getting-most-out-vary-fastly)に関する記事を書きました。(Fastly などの) CDN は、サーバーとユーザーの間に配置できる中間キャッシュです。また、ブラウザは Vary ルールを認識してそれらに対応する必要がありますが、そのプロセスは、CDN による Vary の処理方法とは異なります。この記事では、*ブラウザ*におけるキャッシュのバリエーションについて説明します。
## ブラウザにおける現在の Vary のユースケース
[過去の記事](https://www.fastly.com/blog/getting-most-out-vary-fastly/)でご説明したように、従来の Vary の活用方法は、`Accept`、`Accept-Language`、`Accept-Encoding` の各ヘッダーを使用して[コンテンツネゴシエーション](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation)を実行するというものでしたが、最初の2つのヘッダーはうまく機能していませんでした。`Accept-Encoding` を使用して gzip または brotli で圧縮されたレスポンスを配信するのは (ブラウザでサポートされている限り) 大抵うまくいきますが、最近ではすべてのブラウザで gzip がサポートされているため、この方法に特別なメリットを感じません。
それでは、以下のようなアイデアはどうでしょう。
- ユーザーの画面と同じ幅の画像を配信したい。ユーザーがブラウザのサイズを変更した場合は、新しい画像をダウンロードする (Client Hints による変更)。
- ユーザーがログアウトした場合、そのユーザーのログイン中にキャッシュされたページの使用を回避したい (Cookie をキーとして使用)。
- WebP 画像形式をサポートするブラウザのユーザーは WebP 画像を取得し、それ以外のユーザーは JPEG 画像を取得する。
- 高密度の画面でブラウザを使用する場合、ユーザーは 2x 画像を取得する。ブラウザウィンドウを標準密度の画面に移行して再度ページを読み込んだ場合は 1x 画像を取得する。
## キャッシュの種類
すべてのユーザーが共有する1つの巨大なキャッシュとして機能するエッジキャッシュとは異なり、ブラウザは各ユーザーを対象に特定の用途向けに異なるキャッシュを用意しています。
![caches all the way down - Vary part2](//images.contentful.com/6pk8mg3yh2ee/4ECSZtOwYgOiS0EIES060e/2a38d0de88a218213983a75d8acf31f3/caches_all_the_way_down_-_Vary_part2.png)
これらの一部は新しいキャッシュです。キャッシュコンテンツの読み込み元を正確に把握するには複雑な計算が必要で、これは開発者向けのツールでは十分にサポートされていません。このようなキャッシュを以下に示します。
- __イメージキャッシュ__はページを対象としたキャッシュで、デコードされた画像データを保存します。例えば、1つのページに同じ画像が複数回追加されている場合、ブラウザがその画像をダウンロードしてデコードする必要があるのは1回のみです。
- __[プリロード](https://w3c.github.io/preload)キャッシュ__もページを対象とし、(通常はキャッシュ不可能なリソースであっても) Link ヘッダーまたは `` タグでプリロードされたデータを保存します。イメージキャッシュと同様に、プリロードキャッシュはユーザーがページから移動すると破棄されます。
- __サービスワーカー[キャッシュ API](https://w3c.github.io/ServiceWorker/#cache) は、プログラム可能なインターフェイスを備えたキャッシュバックエンドを提供します。サービスワーカーの JavaScript コードを使用して明確に設定しない限り、このキャッシュには何も保存されません。また、このキャッシュは、サービスワーカーのフェッチハンドラーで明示的に指定した場合にのみチェックされます。サービスワーカーキャッシュは、オリジンを対象としたキャッシュです。永続性は保証されませんが、ブラウザの HTTP キャッシュよりも永続性があります。
- __[HTTP キャッシュ](http://httpwg.org/specs/rfc7234.html)は、最もなじみのあるメインのキャッシュです。HTTP レベルのキャッシュヘッダー (`Cache-Control` など) に対応する唯一のキャッシュです。これらのヘッダーとブラウザ独自のヒューリスティックルールを組み合わせ、データをキャッシュすべきかどうか判断し、キャッシュの期間を決定します。このキャッシュは対象範囲が最も広く、すべてのサイトで共有されます。そのため、関連のない2つのサイトが同じアセット (例えば Google Analytics など) を読み込むと、それらのサイトは同じキャッシュヒットを共有する場合があります。
- 最後に __[HTTP/2 プッシュキャッシュ](http://httpwg.org/specs/rfc7540.html#rfc.section.10.4) (別名 H2 プッシュキャッシュ) は接続と連携し、サーバーからプッシュされたものの、その接続を使用するどのページからもまだリクエストされていないオブジェクトを保存します。このキャッシュは、特定の接続を使用するページを対象とし、単一のオリジンを対象とするのと基本的には同じですが、接続が切断された場合にも破棄されます。
これらの中で、HTTP キャッシュと Service Worker キャッシュが非常によく定義されています。イメージキャッシュとプリロードキャッシュは、一部のブラウザで特定のナビゲーションのレンダリングに関連付けられた単一の「メモリキャッシュ」として実装されている場合がありますが、ここで説明するメンタルモデルはプロセスについての正しい考え方です。興味をお持ちの方は、プリロードの[仕様に関する説明](https://w3c.github.io/preload/#h-note3)をご覧ください。H2 サーバープッシュについては、[このキャッシュの今後に関する積極的な議論](https://github.com/whatwg/fetch/issues/354)が続けられています。
何かをリクエストすると、そのデータがキャッシュ外部のレイヤーから内部のレイヤーにプルされる可能性があるため、ネットワークに送信される前にリクエストがこれらのキャッシュを確認する順序が重要です。例えば、HTTP/2 サーバーがスタイルシートと、そのシートを必要とするページを共にプッシュし、そのページも `` タグを使用してスタイルシートをプリロードする場合、スタイルシートはブラウザ内の3つのキャッシュを経由することになります。まず最初に、H2 プッシュキャッシュでリクエストされるのを待ちます。ブラウザがページをレンダリングしてプリロードタグに到達すると、スタイルシートがプッシュキャッシュからプルされ、HTTP キャッシュを*経由*し (スタイルシートの `Cache-Control` ヘッダーによっては、このキャッシュにスタイルシートが保存される場合があります)、プリロードキャッシュに保存されます。
![Vary part 2](//images.contentful.com/6pk8mg3yh2ee/1UWIlhJUFCS84euIcGCG4e/cad25f8147558bc14c27e46649b6f403/Vary_part_2.png)
## バリデーターとしての Vary の導入
それでは、この状況で `Vary` を使用するとどうなるでしょうか。
(CDN などの) 中間キャッシュとは異なり、ブラウザは通常、__URL ごとに複数のバリエーションを保存する機能を実装していません__。これは、一般的に `Vary` を使用する対象 (主に `Accept-Encoding` と `Accept-Language`) が1人のユーザーのコンテキスト内で頻繁に変化しないと仮定されるためです。`Accept-Encoding` は、ブラウザのアップグレード時に変更される*可能性があるかもしれません* (しかし、おそらく変更されません)。言語が変更される可能性があるのは、OS の言語ロケール設定を変更した場合のみです。また、`Vary` をこのように実装する方がずっと簡単です。ただし、一部の仕様の作成者はこれが間違いであると確信しています。
ほとんどの場合、ブラウザが1つのバリエーションのみを保存しても大きな損失にはなりませんが、レスポンスの変化の基準となるデータが変更される場合に、有効ではなくなったバリエーションを誤って使用しないことが重要です。
妥協案として、`Vary` をキーではなく[バリデーター](http://httpwg.org/specs/rfc7234.html#validation.model)として扱う方法があります。ブラウザではキャッシュキーを通常の方法 (基本的には URL を使用) で計算し、ヒットした場合はキャッシュされたレスポンスに組み込まれた `Vary` ルールをリクエストが満たしているか確認します。ルールを満たしていない場合、ブラウザはそのリクエストをキャッシュミスとして扱い、キャッシュの次のレイヤーに進むか、外部のネットワークに移動します。新しいレスポンスを受信すると、実際には異なるバリエーションであったとしても、キャッシュされたバージョンが上書きされます。
## Vary の動作のデモ
`Vary` の処理方法のデモを行うため、[小規模なテストスイート](https://vary-test.fastlydemo.net)を作成しました。このテストでは、さまざまな URL を読み込み、異なるヘッダーによって変化させ、リクエストがキャッシュをヒットするかどうかを検出します。私は当初、このテストに [ResourceTiming](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API) を使用していましたが、互換性を向上させるためにリクエストが完了するまでの時間を測定する方法に切り替えました (また、違いを明確にするために、サーバー側のレスポンスに対して1秒の遅延を意図的に追加しました)。
それぞれのキャッシュの種類と `Vary` の仕組み、そして `Vary` が実際に想定どおりに機能しているかを確認してみましょう。それぞれのテストについて、想定していたキャッシュの結果 (ヒットまたはミス) と実際の結果を示します。
__プリロード__ : 現在、プリロードは Chrome でのみサポートされています。プリロードされたレスポンスはページから要求されるまでメモリキャッシュに保存されます。また、HTTP キャッシュが可能な場合、レスポンスはプリロードキャッシュに送信される途中で HTTP キャッシュにも保存されます。プリロードを含むリクエストヘッダーを指定することは不可能であり、プリロードキャッシュの生存時間はページと同じであるため、このキャッシュのテストは困難ですが、少なくとも Vary ヘッダーが設定されたオブジェクトが正常にプリロードされることは確認できます。
[![Vary part 2 link preload](//images.contentful.com/6pk8mg3yh2ee/3V8PQSge7uioeKAuiMG8yQ/16248dd51f3114a057447db5f589570e/Vary_part_2_link_preload.png)](https://vary-test.fastlydemo.net/#preload)
__サービスワーカーキャッシュ API__ : サービスワーカーは Chrome と Firefox でサポートされています。サービスワーカーの仕様を策定する際、作成者は壊れた実装と判断される部分を修正し、ブラウザで `Vary` が CDN に近い機能を果たすようにしたいと考えていました。つまり、ブラウザは1つのバリエーションのみを HTTP キャッシュに保存しながら、キャッシュ API に複数のバリエーションを維持するということです。Firefox (54) ではこの処理が正しく行われますが、Chrome では HTTP キャッシュの場合と同様に `Vary` をバリデーターとして扱うロジックが使用されています ([この問題の提起についてはこちらを参照](https://bugs.chromium.org/p/chromium/issues/detail?id=756796))。
[![Vary part 2 serviceworker cache](//images.contentful.com/6pk8mg3yh2ee/4yDTEeNLiMY4KUIyYCk6AM/39a49d4a39eca78d3048a458660d2114/Vary_part_2_serviceworker_cache.png)](https://vary-test.fastlydemo.net/#sw-cache)
__HTTP キャッシュ__ : メインの HTTP キャッシュは `Vary` ヘッダーに従い、(バリデーターとしての) `Vary` の処理をすべてのブラウザで一貫して実行します。詳細については、Mark Nottingham 氏のブログ「[The State of Browser Caching, Revisited](https://www.mnot.net/blog/2017/03/16/browser-caching)」をご覧ください。
__HTTP/2 プッシュキャッシュ__ : 理論的には `Vary` に従うはずですが、実際にこの処理を行うブラウザはありません。ブラウザは、単にプッシュされたレスポンスと、レスポンスを変化させるヘッダーにランダムな値が指定されたリクエストをマッチングし、マッチしたレスポンスを消費します。
[![Vary part 2 h2 push](//images.contentful.com/6pk8mg3yh2ee/39qYr9sx56w8WgqySGWWoG/d305f745406bab335cb422c396fe9c86/Vary_part_2_h2_push.png)](https://vary-test.fastlydemo.net/#h2push)
## 304 Not Modified に関する妙案
__HTTP 304「Not Modified」__は非常に興味深いレスポンスステータスです。私たちの敬愛するリーダー Artur Bergman は、[HTTP キャッシュの仕様](http://httpwg.org/specs/rfc7232.html#status.304)でこの特別なレスポンスについて指摘しています (ここでは Vary を意図的に太字にしています)。
>*304レスポンスを生成するサーバーは、同じリクエストに対して200 (OK) レスポンスで送信される Cache-Control、Content-Location、Date、ETag、Expires、__Vary__ のいずれかのヘッダーフィールドを生成する必要があります。*
304レスポンスはなぜ Vary ヘッダーを返すのでしょうか。これらのヘッダーを含む304レスポンスの受信時に必要な処理に関する以下の記述を読むとさらに謎が深まります。
>*保存されたレスポンスが更新の対象の場合、キャッシュは [...] 304 (Not Modified) レスポンスで指定された他のヘッダーフィールドを使用して、保存されたレスポンスの対応するヘッダーフィールドのすべてのインスタンスを置き換える必要があります。*
これはどういうことでしょうか。304の `Vary` ヘッダーと既存のキャッシュされたオブジェクトのヘッダーが異なる場合、キャッシュされたオブジェクトが更新されることになりますが、それによりキャッシュされたオブジェクトがリクエストに合致しなくなる可能性があります。
このシナリオでは一見、__キャッシュされたバージョンの使用が可能であることと不可能であることを304が同時に通知しているようにみえます__。もちろん、サーバーがキャッシュされたバージョンを実際に使用させたくないのであれば、304ではなく200を送信するでしょう。つまり、キャッシュされたバージョンが確実に使用されるものの、それに対して更新を行った後は、最初にキャッシュが行われたリクエストとまったく同一のリクエストを今後受信した場合にキャッシュされたバージョンを再び使用できない可能性があるということを意味します。
*(備考 : Fastly では、この仕様の動作を考慮しないため、オリジンサーバーから304を受信した場合、TTL をリセットする以外は、キャッシュにある未修整のオブジェクトを引き続き使用します。)*
各種ブラウザでは[この動作が考慮されている](https://vary-test.fastlydemo.net/#304-nomatch)ようですが、特殊な処理を必要とします。更新後、キャッシュされているレスポンスが現在のリクエストと確実に合致するように、ブラウザではレスポンスヘッダーだけでなく、それとペアになるリクエストヘッダーも更新されます。これは理にかなっているように思われます。仕様にはこのことが記載されていないため、ブラウザはそれぞれ自由に処理を行うことが可能ですが、幸いなことにすべてのブラウザで動作が統一されています。
## Client Hints
Google の [Client Hints](http://httpwg.org/http-extensions/client-hints.html) は、ブラウザで久々に Vary に対して追加された最も重要な新機能の1つです。`Accept-Encoding` や `Accept-Language` とは異なり、Client Hints はユーザーが Web サイト内を移動するのに伴い定期的に変化する可能性のある値を示します。具体的には以下の値です。
- __DPR__
(デバイスピクセル比) 画面のピクセル密度 (ユーザーが複数の画面を使用している場合は変化する可能性があります)
- __Save-Data__
ユーザーがデータセーブモードを有効にしているか
- __Viewport-Width__
現在のビューポートのピクセル幅
- __Width__
必要なリソース幅 (物理ピクセル数)
これらの値は1人のユーザーについて変わる可能性があるだけでなく、幅に関連する値の範囲が大きくなります。したがって、これらのヘッダーで `Vary` を使用することは可能ですが、キャッシュ効率が低下したり、キャッシュの効果が無くなる恐れがあります。
## Key ヘッダーに関する提案
`Vary` は長年にわたり使用されてきましたが、最近になって `Vary` を新しい [Key](http://httpwg.org/http-extensions/key.html) ヘッダーに置き換えようという提案がされています。いくつかの例を見てみましょう。
```
Key: Viewport-Width;div=50
```
この例では、リクエストヘッダーの `Viewport-Width` の値に基づいてレスポンスが変化しますが、最も近い50ピクセルの倍数に切り捨てられます。
```
Key: cookie;param=sessionAuth;param=flags
```
このヘッダーのレスポンスへの追加は、2つの特定の Cookie (`sessionAuth` と `flags`) によってレスポンスが変化することを意味します。これらが変更されていなければ、このレスポンスを将来のリクエストに再利用できます。
`Key` と `Vary` の主な違いは以下のとおりです。
- `Key` を使用すると、ヘッダー内の__サブフィールド__ による変化が許可され、1つまたは複数の Cookie によるレスポンスの変化が可能になります。つまり、事実上、任意のどのデータ (例えば*ユーザーのログインステータス*) によるレスポンスの変化も可能になります。
- 個々の値を__範囲にバケット化__し、キャッシュヒットの変化を増大させることができ、特にビューポート幅などのデータに基づく変化に役立ちます。
- 同一の URL を持つすべてのバリエーションには同じ `Key` を設定する必要があります。したがって、既存のバリアントがキャッシュにいくつか存在する URL に対する新しいレスポンスをキャッシュが受信し、新しいレスポンスの `Key` ヘッダーの値が既存のバリアントの値と一致しない場合、すべてのバリアントがキャッシュから削除されます。
この記事を書いている時点では、`Key` をサポートするブラウザや CDN は存在しませんが、一部の CDN では受信ヘッダーを複数のプライベートヘッダーに分割し、それらに基づいて変化するように設定することで同じ効果が得られます (Fastly でこの処理を行う手順については、[こちらをご覧ください](https://www.fastly.com/blog/getting-most-out-vary-fastly))。つまり、`Key` が効力を発揮できる主な領域はブラウザです。
すべてのバリエーションに同じ Key レシピを設定するという要件のために Key の活用がある程度限定されます。[この仕様の「早期終了」のオプション](https://github.com/httpwg/http-extensions/issues/232)に今後注目したいところです。これにより、「認証状態に基づいてレスポンスが変化し、ログインしている場合は環境設定によっても変化する」などの処理を行うことができます。
## バリアントに関する提案
`Key` は優れた汎用メカニズムですが、一部のヘッダーの値にはさらに複雑なルールが設定されています。このような値のセマンティクスを理解することは、キャッシュのバリエーションを自動的に減らす方法を見つけるのに役立ちます。例えば、`Accept-Language` の値が異なる (`en-gb` および `en-us`) 2つのリクエストが送信され、Web サイトではサポートする言語のバリエーションが「English (英語)」の1つのみであると想像してください。US English (アメリカ英語) のリクエストに応答し、レスポンスが CDN にキャッシュされた場合、UK English (イギリス英語) のリクエストにそのレスポンスを再利用することはできません。`Accept-Language` の値が異なり、両方のリクエストに同じレスポンスを返しても問題ないということをキャッシュが認識できないためです。
ここで、大きなファンファーレと共に[バリアント](https://mnot.github.io/I-D/variants/)に関する提案の登場です。この提案では、サーバーがサポート対象のバリアントを示すことができるようにします。これにより、実質的に異なるバリエーションと同一のバリエーションに関する判断をキャッシュが効果的に下せるようになります。
現時点ではバリアントは極めて初期のドラフト段階にあり、`Accept-Encoding` と `Accept-Language` を支援する目的で設計されているため、その有用性はブラウザキャッシュではなく CDN などの共有キャッシュに限定されています。しかし、`Key` と組み合わせて使用することでキャッシュのバリエーションをより効果的にコントロールできるようになります。
## 最後に
この記事ではブラウザにおける Vary ヘッダーの機能についていろいろみてきました。ブラウザの内部でどのような処理が行われているのかを理解するのは興味深いと同時に、そこからいくつかのシンプルな結論を導くこともできます。
- ほとんどのブラウザは `Vary` をバリデーターとして使用しています。複数のバリエーションがキャッシュされるようにしたい場合は、代わりに異なる URL を使用する方法を見つけることをお勧めします。
- ブラウザは、HTTP/2 サーバープッシュを使用してプッシュされるリソースに対して `Vary` を無視します。そのため、プッシュするデータに基づいてレスポンスを変化させないようにしてください。
- ブラウザには多数のキャッシュがあり、その機能はそれぞれ異なります。キャッシュの決定プロセスがそれぞれのパフォーマンスに (特に `Vary` のコンテキストにおいて) 及ぼす影響を把握してください。
- `Vary` は必ずしも想定通りに役立つとは限りません。`Key` と Client Hints の組み合わせにより、`Vary` の有用性が変わり始めています。これらを使い始めるタイミングについては、[ブラウザによるサポート](http://caniuse.com/client-hints-dpr-width-viewport)に関するサイトをご覧ください。