HTTP キャッシュ
概要
HTTP キャッシュは、リクエストに関連付けられたレスポンスを格納し、格納されたレスポンスを後続のリクエストのために再利用します。
再利用にはいくつかの利点があります。まず、リクエストを元サーバーに届ける必要がないため、クライアントとキャッシュの距離が近いほど、レスポンスが速くなります。ほとんどの例は、ブラウザー自身がブラウザーのリクエストのキャッシュを格納するものです。
また、レスポンスが再利用可能な場合、元のサーバーはリクエストを処理する必要がありません。つまり、リクエストを解釈してルーティングしたり、クッキーに基づいてセッションを復元したり、結果をDBに問い合わせたり、テンプレートエンジンをレンダリングしたりする必要がありません。これによりサーバーの負荷が軽減されます。
キャッシュを適切に管理運営することは、システムを健全にするためにとても重要です。
キャッシュの種類
HTTP Caching 仕様書では、キャッシュには大きく分けて 2 つの種類があります。プライベートキャッシュと共有キャッシュです。
プライベートキャッシュ
プライベートキャッシュとは、特定のクライアント(通常はブラウザーキャッシュ)に結びついたキャッシュのことです。格納されるレスポンスは他のクライアントと共有されないので、プライベートキャッシュはそのユーザー用にパーソナライズされたレスポンスを格納することができます。
一方、プライベートキャッシュ以外のキャッシュにパーソナライズされたコンテンツが格納されると、他のユーザーがそのコンテンツを取得できる可能性があり、意図しない情報漏洩が発生する可能性があります。
レスポンスにパーソナライズされたコンテンツを含み、プライベートキャッシュにのみ格納したい場合は private
ディレクティブを指定しなければなりません。
Cache-Control: private
パーソナライズされたコンテンツは通常クッキーによって制御されますが、クッキーの存在が常にプライベートであることを示すとは限らないため、クッキーだけでレスポンスがプライベートになるわけではありません。
レスポンスに Authorization
ヘッダーがある場合、プライベートキャッシュには格納されない(public
を指定しない限り、共有キャッシュに格納される)ことに注意してください。
共有キャッシュ
共有キャッシュはクライアントとサーバーの間に格納され、ユーザー間で共有できるレスポンスを格納するために使用されます。共有キャッシュはさらに、プロキシーキャッシュ と マネージドキャッシュ に分類されます。
プロキシーキャッシュ
アクセス制御する機能に加えて、一部のプロキシーはネットワークからのトラフィックを削減するためにキャッシュを実装しています。これは通常サービス開発者によって管理されるものではないので、適切な HTTP ヘッダーなどによって 制御しなければなりません。しかし、過去には時代遅れのプロキシーキャッシュ実装、すなわち HTTP キャッシュ標準を正しく理解していない実装などが、開発者に問題を発生させることが多くありました。
以下のようなキッチンシンクヘッダーを用いて、no-store
のような現在の HTTP Caching 仕様書のディレクティブを理解しない、「古くて更新されていないプロキシーキャッシュ」の実装を回避しようします。
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate
しかし、近年 HTTPS が一般的になり、クライアント/サーバ間の通信が暗号化されるようになったため、 経路上のプロキシーキャッシュはレスポンスをトンネリングすることしかできず、 キャッシュとして振る舞うことができない場合が多くなっています。そのような場合は、レスポンスを見ることすらできないので、時代遅れのプロキシーキャッシュ実装を心配する必要はありません。
他にも、 TLS ブリッジプロキシーが、組織が管理する認証局 (CA) の資格情報を PC にインストールすることで、中間者 (person-in-the-middle) 的にすべての通信を復号し、アクセス制御などを行う場合、レスポンスの内容を見たり、キャッシュしたりすることが使用可能です。ただし、近年は CT (certificate transparency) が普及し、ブラウザーによっては SCT (signed certificate timestamp) で発行された証明書しか許可しないものもあるため、このメソッドでは企業ポリシーを適用する必要があります。このような制御された環境では、プロキシーキャッシュが「古くて更新されない」ことを心配する必要はありません。
マネージドキャッシュ
マネージドキャッシュは、サービス開発者が元サーバの負荷を分散し、コンテンツを効率的に開発するために明示的に展開するものです。例えば、リバースプロキシー、CDN、キャッシュ API と組み合わせたサービスワーカーなどがあります。
マネージドキャッシュの特性は、展開する製品によって異なります。ほとんどの場合、Cache-Control
ヘッダーと自分自身で設定ファイルやダッシュボードを通してキャッシュの動作を制御することができます。
例えば、HTTP キャッシュ仕様書では、基本的にキャッシュを明示的に削除する方法は定義されていません。しかし、マネージドキャッシュでは、ダッシュボード操作や API 呼び出し、再起動などによって、格納されたレスポンスをいつでも削除することができます。これにより、より積極的なキャッシュ戦略が可能になります。
標準 HTTP キャッシュ仕様書のプロトコルを無視して、明示的な操作を行うことも使用可能です。例えば、次の例はプライベートキャッシュやプロキシキャッシュをオプトアウトし、自分自身で管理キャッシュにのみキャッシュする戦略を使用するように指定することができます。
Cache-Control: no-store
例えば、Varnish Cache は VCL (Varnish Configuration Language、DSL の一種)ロジックを使用してキャッシュストレージを処理しますが、サービスワーカーとキャッシュ API との組み合わせにより、JavaScript でそのロジックを作成することができます。
つまり、マネージドキャッシュが意図的に no-store
ディレクティブを無視したとしても、それを「標準に準拠していない」とみなす必要はないということです。キッチンシンクヘッダーの使用を避け、使用している管理キャッシュ機構のドキュメントを注意深く読み、使用する機構で指定された方法で適切にキャッシュを制御していることを確実に保持することです。
なお、CDNによっては、そのCDNに対してのみ有効なヘッダーを自分自身で提供しているものもあります(Surrogate-Control
など)。現在、これらを標準化するために CDN-Cache-Control
ヘッダーを定義する作業が進められています。
ヒューリスティックキャッシュ
HTTP は可能な限りキャッシュするように設計されているので、Cache-Control
が指定されていなくても、ある条件が満たされればレスポンスは格納されて再利用されます。これはヒューリスティックキャッシュと呼ばれます。
例えば、次のようなレスポンスを見てみましょう。このレスポンスの最終更新は 1 年前です。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT
<!doctype html>
…
1 年間更新されなかったコンテンツは、その後しばらく更新されないことがヒューリスティックに知られています。そのため、クライアントはこのレスポンスを(max-age
がないにもかかわらず)格納し、しばらく再利用します。どのくらいの期間再利用するかは実装次第ですが、仕様では格納した後の時間の約 10 %(この場合は 0.1 年)を推奨しています。
ヒューリスティックキャッシュは Cache-Control
対応が広く採用される以前の回避策であり、基本的にすべてのレスポンスは Cache-Control
ヘッダーを明示的に指定する必要があります。
age に基づく新鮮さと古さ
格納される HTTP レスポンスには、新鮮 (fresh) と古い (stale) の 2 つの状態があります。新鮮な状態は通常、レスポンスがまだ有効で再利用できることを示し、古い状態はキャッシュされたレスポンスがすでに期限切れであることを意味しています。
レスポンスがいつ新鮮で、いつ古くなったかを判断する基準は age です。 HTTP では、 age はレスポンスが生成されてからの経過時間です。これは他にもキャッシュ機構における TTL に似ています。
次のレスポンス例を見てみましょう(604800 秒は 1 週間)。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
<!doctype html>
…
この例のレスポンスが格納されたキャッシュは、レスポンスが生成されてからの経過時刻を計算し、その結果をレスポンスの age として使用します。
例えば、レスポンスの max-age
の意味は以下の通りです。
- レスポンスの age が 1 週間未満である場合、そのレスポンスは新鮮です。
- レスポンスの age が 1 週間を超える場合、そのレスポンスは古いです。
格納されたレスポンスが新鮮である限り、クライアントのリクエストを履行するために使用されます。
レスポンスが共有キャッシュに格納されるとき、クライアントにレスポンスの age を通知する必要があります。例えば、共有キャッシュがレスポンスを 1 日間格納された場合は、次の例のようになります。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400
<!doctype html>
…
そのレスポンスを受け取ったクライアントは、レスポンスの max-age
と Age
の差である残り 518400 秒の間、そのレスポンスが新鮮であることを受け取ります。
有効期限または max-age
HTTP/1.0 では、鮮度は Expires
ヘッダーを使用して指定 していました。
Expires
ヘッダーは、経過時間を指定するのではなく、明示的な時刻を使用してキャッシュの有効期間を指定します。
Expires: Tue, 28 Feb 2022 22:22:22 GMT
しかし、この時刻形式は解釈が難しく、多くの実装バグが見つかり、システムクロックを意図的にずらすことで問題を誘発する可能性がありました。そのため、HTTP/1.1 では Cache-Control
に経過時間を指定する max-age
が採用されました。
Expires
と Cache-Control: max-age
の両方が利用できる場合は、 max-age
を優先するように定義されています。そのため、HTTP/1.1 が広く使用されるようになった現在では Expires
を提供する必要はありません。
Vary
レスポンスを判別する方法は、基本的に URL に基づいています。
しかし、同じ URL を持っていてもレスポンスのコンテンツが常に同じとは限りません。特にコンテンツネゴシエーションが行われた場合、サーバからのレスポンスは Accept
、Accept-Language
、Accept-Encoding
リクエストヘッダーの値によって変わる可能性があります。
例えば、Accept-Language: en
ヘッダーで返されてキャッシュされた英語のコンテンツに対して、Accept-Language: ja
リクエストヘッダーがあるリクエストに対してキャッシュされたレスポンスを再利用することは望ましくありません。この場合、"Accept-Language
" を Vary
ヘッダーの値に追加することで、レスポンスが言語に基づいて別個にキャッシュされるようにすることができます。
Vary: Accept-Language
これにより、キャッシュはレスポンス URL と Accept-Language
リクエストヘッダーの合成に基づいて生成されます。
また、(例えばレスポンシブデザインのために)ユーザーエージェントに基づいてコンテンツの最適化を提供している場合、 Vary
ヘッダーの値に "User-Agent
" を含めることができます。しかし、一般的に User-Agent
リクエストヘッダには非常に多くのバリエーションがあり、キャッシュが再利用される可能性を大幅に縮小します。そのため、可能であれば User-Agent
リクエストヘッダーに基づくのではなく、機能検出に基づいて動作を変化させる方法を検討してください。
キャッシュされたパーソナライズされたコンテンツが他に再利用されるのを防ぐために、クッキーを使用するアプリケーションでは、Vary
にクッキーを指定する代わりに Cache-Control: private
を指定する必要があります。
検証
古くなったレスポンスはすぐには破棄されません。HTTPには、元のサーバーに問い合わせることで、古いレスポンスを新しいものに変換する仕組みがあります。これは検証、または再認証と呼ばれます。
検証は If-Modified-Since
または If-None-Match
リクエストヘッダーを含む条件付きリクエストを使用することで行われます。
If-Modified-Since
以下のレスポンスは 22:22:22 に生成され、 max-age
は 1 時間なので、 23:22:22 まで新鮮であることがわかります。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
<!doctype html>
…
23:22:22 になると、レスポンスは古くなり、キャッシュを再利用できなくなります。そのため、下記のリクエストはクライアントが If-Modified-Since
リクエストヘッダーを持つリクエストを送信し、指定した時刻以降に変更があったかどうかをサーバーに問い合わせることを示しています。
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT
コンテンツが指定した時刻から変更されていない場合、サーバーは 304 Not Modified
というレスポンスを返します。
このレスポンスは「変更されていない」ことを示すだけなので、レスポンス本体はありません。ステータスコードだけですので、転送サイズは非常に小さくなります。
HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600
そのレスポンスを受信すると、クライアントは格納される古いレスポンスを新しいものに戻し、残りの 1 時間の間に再利用することができます。
サーバーはオペレーティングシステムのファイルシステムから更新時刻を取得することができ、静的ファイルを提供する場合には相対的に容易です。しかし、いくつかの問題があります。例えば、時刻形式が複雑で解釈しにくい、分散サーバーではファイル更新時刻の同期が取りにくいなどです。
このような問題を解決するために、 ETag
レスポンスヘッダーが標準化されました。
ETag/If-None-Match
ETag
レスポンスヘッダーの値は、サーバーが生成する任意の値です。サーバーがどのように値を生成しなければならないかという制限はないので、サーバーはどのような意味に基づいて値を設定しても自由です。例えば、本体のコンテンツのハッシュやバージョン番号などです。
例えば、 ETag
ヘッダーにハッシュ値を用い、 index.html
リソースのハッシュ値が 33a64df5
の場合、レスポンスは以下のようになります。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "33a64df5"
Cache-Control: max-age=3600
<!doctype html>
…
そのレスポンスが古かった場合、クライアントはキャッシュされたレスポンスの ETag
レスポンスヘッダーの値を受け取り、それを If-None-Match
リクエストヘッダーに入れ、リソースが変更されたかどうかをサーバーに問い合わせます。
GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-None-Match: "33a64df5"
リクエストされたリソースの ETag
ヘッダーの値がリクエストの If-None-Match
の値と同じ場合、サーバーは 304 Not Modified
を返します。
しかし、リクエストされたリソースがこれで異なる ETag
値を持つようになったとサーバが判断した場合、サーバは代わりに 200 OK
とリソースの最新バージョンで応答します。
メモ:
RFC9110 では、できれば 200
レスポンスに対して ETag
と Last-Modified
の両方を送信することを推奨しています。
キャッシュの再検証の際に、 If-Modified-Since
と If-None-Match
の両方が存在する場合は、 If-None-Match
を優先します。
キャッシュだけを考えているのであれば、 Last-Modified
は不要だと思うかもしれません。
しかし、 Last-Modified
はキャッシュに有益なだけではなく、コンテンツ管理 (CMS) システムが最終更新時刻を表示したり、クローラーがクロール頻度を調整したり、他にも様々な目的で使用する標準 HTTP ヘッダーです。
そのため、 HTTP エコシステム全体を考慮すると、 ETag
と Last-Modified
の両方を提供した方が良いでしょう。
強制的な再検証
レスポンスを再利用せず、常に最新のコンテンツをサーバーから取得したい場合は、 no-cache
ディレクティブを使用して強制的に検証することができます。
レスポンスに Cache-Control: no-cache
を Last-Modified
と ETag
とともに追加することで、下記で示すように、リクエストされたリソースが更新されていればクライアントは 200 OK
レスポンスを受け取り、更新されていなければ 304 Not Modified
レスポンスを受け取ります。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache
<!doctype html>
…
max-age=0
と must-revalidate
の組み合わせは no-cache
と同じ意味を持つとよく言われています。
Cache-Control: max-age=0, must-revalidate
max-age=0
はレスポンスが即座に古くなることを意味しており、 must-revalidate
は古くなったレスポンスを再検証せずに再利用してはいけないことを意味しています。つまり、この組み合わせの意味は no-cache
と同じです。
しかし、この max-age=0
を使用することは、HTTP/1.1 より前の実装の多くが no-cache
ディレクティブを処理することができなかったことの名残です。そのため、その制限に対処するために、回避策として max-age=0
を使用していました。
しかし、これで HTTP/1.1 準拠のサーバが広く普及したため、 max-age=0
と must-revalidate
の組み合わせを使用する理由はなくなりました。
キャッシュしない
no-cache
ディレクティブはレスポンスを格納しないようにするのではなく、再検証せずにレスポンスを再利用しないようにします。
レスポンスをキャッシュに格納したくない場合は、 no-store
を使用してください。
Cache-Control: no-store
しかし、一般的に、「キャッシュしない」という要件は、実際には以下のような設定になります。
- プライバシー保護のため、特定のクライアント以外にレスポンスを格納されたくない場合。
- 常に最新の情報を提供したい場合。
- 時代遅れの実装であり、何が起こるか分からない場合。
このような状況では、 no-store
は常に適切なディレクティブとは限りません。
次の節では、その状況をより詳しく見ていきます。
他の人と共有しないようにする
パーソナライズされたコンテンツを含むレスポンスが、予期せずキャッシュの他のユーザーに見えてしまうのは問題です。
このような場合、private
ディレクティブを使用することで、パーソナライズされたレスポンスは特定のクライアントにのみ格納され、他のキャッシュユーザーに漏れることはなくなります。
Cache-Control: private
このような場合、 no-store
を指定したとしても、 private
も指定しなければなりません。
常に最新のコンテンツを提供する
no-store
ディレクティブはレスポンスが格納されることを防ぎますが、同じ URL に対して既に格納されているレスポンスを削除することはありません。
言い換えれば、特定の URL に対して古いレスポンスが既に格納されている場合、 no-store
を返しても古いレスポンスが再利用されることはありません。
しかし、no-cache
ディレクティブは格納されたレスポンスを再利用するために、 クライアントに検証リクエストを送らせます。
Cache-Control: no-cache
サーバーが条件付きリクエストに対応していない場合は、クライアントに毎回サーバーにアクセスするように強制し、常に 200 OK
で最新のレスポンスを取得することができます。
古い実装への対応
no-store
を無視する古い実装を回避するために、以下のようなキッチンシンクのようなヘッダーが使用されているかもしれません。
Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate
推奨 されている方法は、このような古い実装に対処するために代替手段として no-cache
を使用することです。また、no-cache
が最初から指定されていても、サーバーは常にリクエストを受け取るので問題ありません。
もし気になるのが共有キャッシュであれば、 private
も追加することで、意図しないキャッシュを防ぐことができます。
Cache-Control: no-cache, private
no-store
で失われるもの
no-store
を追加することは、キャッシュをオプトアウトするために正しい方法だと思うかもしれません。
しかし、no-store
を自由に許可することは推奨されません。ブラウザーのバック/フォワードキャッシュを含めるために、 HTTP とブラウザーが持つ多くの利点を失ってしまうからです。
したがって、ウェブプラットフォームの全機能の利点を享受するには、 no-cache
を private
と組み合わせて使用することを推奨します。
再読み込みと強制再読み込み
検証はレスポンスだけでなく、リクエストに対しても行うことができます。
再読み込み (reload) と強制再読み込み (force reload) は、ブラウザー側から実行される検証の一般的な例です。
再読み込み
ウィンドウの破損からの回復やリソースの最新バージョンへの更新のために、ブラウザーはユーザーに再読み込み機能を提供しています。
ブラウザーの再読み込み中に送信される HTTP リクエストを簡略化すると、以下のようになります。
GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If-None-Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT
(Chrome、Edge、Firefox からのリクエストは上記のものにとてもよく似ています。 Safari からのリクエストは多少異なります。)
リクエストの max-age=0
ディレクティブは、「age が 0 以下のレスポンスの再利用」を指定します。つまり、事実上、中間で格納されたレスポンスは再利用されません。
その結果、リクエストは If-None-Match
と If-Modified-Since
によって検証されます。
この動作はフェッチ標準でも定義されており、JavaScript でキャッシュモードを no-cache
に設定して fetch()
を呼び出すことで再現することができます(この場合 reload
は正しいモードではないことに注意してください)。
// メモ: "reload" は通常の再読み込みには正しいモードではありません。 "no-cache" です。
fetch("/", { cache: "no-cache" });
強制再読み込み
ブラウザーがリロード時に max-age=0
を使用するのは、 HTTP/1.1 より前の古い実装の多くが no-cache
を理解していなかったためです。しかし、 no-cache
はこの用途ではこれで問題なく、強制再読み込みはキャッシュされたレスポンスをバイパスするための追加の方法です。
ブラウザーが 強制再読み込みしているときの HTTP リクエストは以下のようになります。
GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache
(Chrome、Edge、Firefox からのリクエストは上記のものにとてもよく似ています。 Safari からのリクエストは多少異なります。)
これは no-cache
による条件付きリクエストではないので、元のサーバーから 200 OK
を取得することは確実です。
この動作はフェッチ標準でも定義されており、JavaScript でキャッシュモードを reload
にして fetch()
を呼び出すことで再現できます(force-reload
ではないことに注意してください)。
// メモ: "reload" ("no-cache" ではない)は「強制再読み込み」で正しいモードです。
fetch("/", { cache: "reload" });
再検証の回避
変更されることのないコンテンツには、キャッシュバスティング、つまり、リクエスト URL にバージョン番号やハッシュ値などを含めることにより、長い max-age
を指定してください。
しかし、ユーザーが再読み込みすると、たとえサーバーがそのコンテンツが不変であることを知っていたとしても、再検証リクエストが送られます。
これを防ぐために、immutable
ディレクティブを使用して、コンテンツは変更されないので再検証は不要であることを明示することができます。
Cache-Control: max-age=31536000, immutable
これにより、再読み込み時の不要な再検証を防ぐことができます。
このディレクティブを実装する代わりに、 Chrome は実装を変更し、サブリソースの再読み込み中に再検証を行わないようにしました。
格納されたレスポンスの削除
長い max-age
で格納されているレスポンスを削除する方法は基本的にありません。
例えば、以下のように https://example.com/
からのレスポンスが格納されていたとします。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000
<!doctype html>
…
サーバー上で有効期限が切れたレスポンスを上書きしたいと思うかもしれませんが、レスポンスが格納されると、キャッシュによりそれ以上のリクエストがサーバーに到達しないため、サーバー側でできることはまだありません。
詳細仕様で言及されているメソッドのひとつに、 POST
のような安全でないメソッドで同じ URL のリクエストを送るというものがありますが、通常多くのクライアントにとって意図的に行うことは困難です。
Clear-Site-Data:cache
ヘッダーと値の仕様もありますが、すべてのブラウザーが対応しているわけではありません。また、使用されている場合でも、ブラウザーのキャッシュにのみ影響し、中間キャッシュには影響を及ぼしません。
したがって、格納されるレスポンスはユーザーが手動で再読み込み、強制再読み込み、履歴クリアの操作を行わない限り、その max-age
期間は残るものとみなされます。
キャッシュはサーバーへのアクセスを削減し、サーバーがその URL の制御を失うことを意味しています。サーバーが URL の制御を失いたくない場合、例えばリソースが頻繁に更新されるような場合、 no-cache
を追加して、サーバが常にリクエストを受け取り、意図するレスポンスを送信できるようにする必要があります。
リクエストの折りたたみ
共有キャッシュは主に元のサーバーの前にあり、元のサーバーへのトラフィックを削減することを意図しています。
したがって、複数の同じリクエストが同時に共有キャッシュに到着した場合、 中間キャッシュはそれ自身に代わって単一のリクエストをオリジンに転送し、 その結果をすべてのクライアントに再利用することがあります。これはリクエストの折りたたみと呼ばれます。
リクエストの折りたたみはリクエストが同時に到着したときに起こるので、レスポンスに max-age=0
や no-cache
が指定されたとしても、再利用されます。
レスポンスが具体的なユーザーにパーソナライズされていて、折りたたみ時に共有されたくない場合は private
ディレクティブを追加してください。
良くあるキャッシュパターン
Cache-Control
の仕様には多くのディレクティブがあり、すべてを理解するのは難しいかもしれません。しかし、ほとんどのウェブサイトは一握りのパターンの組み合わせでカバーすることができます。
この章では、キャッシュを設計する際の一般的なパターンを記述します。
既定の設定
上記のように、キャッシュの既定値(つまり Cache-Control
のないレスポンス)は単純に「キャッシュしない」ではなく、いわゆる「ヒューリスティックキャッシュ」に従った暗黙的なキャッシュです。
ヒューリスティックキャッシュを避けるためには、すべてのレスポンスに既定値の Cache-Control
ヘッダーを明示的に付与することが推奨されます。
既定値では常に最新バージョンのリソースを転送するように保持するために、既定値の Cache-Control
に no-cache
を含めるのが一般的です。
Cache-Control: no-cache
また、サービスワーカースクリプトが Cookie や他のログインメソッドを実装しており、コンテンツがユーザーごとにパーソナライズされている場合は、他のユーザーと共有されないように private
も指定されなければなりません。
Cache-Control: no-cache, private
キャッシュの破棄
キャッシュと最もうまく動作するリソースは、コンテンツが変更されることのない静的で不変なファイルです。そして、変更されるリソースについては、コンテンツが変更されるたびに URL を変更し、 URL 単位を詳しくキャッシュできるようにするのが一般的なベストプラクティスです。
例えば、次のような HTML を考えてみましょう。
<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>
現代のウェブ開発では、開発の進捗に応じて JavaScript や CSS のリソースが頻繁に更新されます。また、クライアントが使用する JavaScript や CSS リソースのバージョンがずれていると、表示が崩れてしまいます。
そのため、上記の HTML では bundle.js
と build.css
を max-age
でキャッシュすることが難しくなっています。
従って、 JavaScript と CSS をバージョン番号やハッシュ値に基づいて変更する部分を含めた URL で提供することができます。その方法のいくつかを下記に示します。
# ファイル名にバージョンを入れる bundle.v123.js # クエリーにバージョンを入れる bundle.js?v=123 # ファイル名にハッシュを入れる bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js # クエリーにハッシュを入れる bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
キャッシュは URL に基づいてリソースを判別するので、リソースが更新されたときに URL が変更されると、キャッシュは再利用されません。
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>
その設計であれば、 JavaScript も CSS リソースも長時間キャッシュすることができます。では、 max-age
はどれくらいの長さに設定するべきなのでしょうか? QPACK 仕様がその答えを提供しています。
QPACK は HTTP ヘッダーフィールドを圧縮するための標準であり、一般的に使用するフィールド値の表が定義されています。
一般的に使用するキャッシュヘッダーの値を下記に示します。
36 cache-control max-age=0 37 cache-control max-age=604800 38 cache-control max-age=2592000 39 cache-control no-cache 40 cache-control no-store 41 cache-control public, max-age=31536000
これらの番号付きオプションのいずれかを選択すると、 HTTP3 で転送するときに値を 1 バイトに圧縮することができます。
番号 37
, 38
, 41
は 1 週間、 1 ヶ月、 1 年の期間です。
キャッシュは新しい項目が保存されると古い項目を除去するので、 max-age
が 1 週間に設定されていたとしても、 1 週間後に格納されるレスポンスが存在する確率はそれほど高くありません。したがって、実際にはどちらを選んでもあまり違いはありません。
41
番が最も長い max-age
(1 年)を持っていますが、 public
が付いていることに注意してください。
public
値は Authorization
ヘッダーを表示している場合でも、レスポンスを保存できるようにする効果があります。
メモ: public
ディレクティブは Authorization
ヘッダーが設定されたときにレスポンスを格納する必要がある場合にのみ使用してください。
max-age
が指定されている限り、レスポンスは共有キャッシュに格納されるからです。
そのため、レスポンスが基本的な認証でパーソナライズされている場合、 public
があると問題が発生する可能性があります。もしそれが心配なら、 2 つ目の値である 38
(1 ヶ月)を選ぶことができます。
# bundle.v123.js へのレスポンス
# 認証によりパーソナライズされることがない場合
Cache-Control: public, max-age=31536000
# 確信が持てない場合
Cache-Control: max-age=2592000
検証
再読み込み時にリソースを再送信する必要がないように、 Last-Modified
ヘッダーと ETag
ヘッダーを設定することを忘れないでください。ビルド済みの静的ファイルに対してこれらのヘッダーを生成するのは簡単です。
ここでの ETag
値はファイルのハッシュです。
# bundle.v123.js へのレスポンス
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
さらに、 immutable
を追加することで、再読み込み時の検証を防ぐことができます。
組み合わせた結果を下記に示します。
# bundle.v123.js
HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK
キャッシュ破棄は、コンテンツが変更されたときに URL を変更することで、長期間にわたってレスポンスをキャッシュ可能にするテクニックです。このテクニックは、画像などのすべてのサブリソースに適用できます。
メモ: immutable
と QPACK を使用することを評価する場合:
もし immutable
が QPACK が提供する定義済みの値を変更することを懸念しているのであれば、 Cache-Control
値を 2 行に分割することで、immutable
の部分を別個の文字にエンコードすることができると考えることができます。ただし、これは特定の QPACK 実装が用いるエンコーディングアルゴリズムに依存します。
Cache-Control: public, max-age=31536000
Cache-Control: immutable
主要リソース
メインリソースはサブリソースと異なり、URLの装飾ができないため、キャッシュ破棄ができません。
次の HTML そのものが格納されている場合、サーバー側でコンテンツが更新されても最新版を表示することはできません。
<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>
その場合、 HTML を格納するためではなく、常に最新の状態にしたいだけなので、 no-store
ではなく、 no-cache
が適切でしょう。
さらに、 Last-Modified
と ETag
を追加することで、クライアントは条件付きのリクエストを送ることができるようになり、HTML に更新がない場合は 304 Not Modified
を返すことができるようになります。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
この設定はパーソナライズされていない HTML には適切ですが、例えばログイン後など、クッキーを使用してパーソナライズされるレスポンスには private
を指定することを忘れないでください。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly
同じことは、favicon.ico
、manifest.json
、.well-known
、キャッシュ破棄を使用して URL を変更できない API エンドポイントにも使用することができます。
ほとんどのウェブコンテンツは、前述した 2 つのパターンの組み合わせに応じた対応が可能です。
マネージドキャッシュの詳細
前節で説明したメソッドでは、キャッシュ破棄を使用することでサブリソースを長時間キャッシュすることができますが、メインリソース(通常は HTML 文書)はキャッシュすることができません。
メインリソースのキャッシュが難しいのは、HTTP キャッシュ仕様の標準ディレクティブを使用するだけでは、サーバーでコンテンツが更新されたときにキャッシュの内容を積極的に削除する方法がないからです。
しかし、CDN やサービスワーカーなどのマネージドキャッシュを展開することで使用可能です。
例えば、API やダッシュボードの処理によってキャッシュの破棄が可能な CDN では、メインのリソースを格納し、サーバーで更新が発生した場合にのみ関連するキャッシュを明示的に破棄することで、より積極的なキャッシュ戦略が可能になります。
サービスワーカーは、サーバで更新が発生したときにキャッシュ API のコンテンツを削除できれば、同じことができます。
より詳しい情報は、 CDN のドキュメント化や サービスワーカーのドキュメント を参照してください。