- はじめに
- TL;DR
- キャッシュの種類
- Cache-Control によるキャッシュの制御
- ETag によるキャッシュの検証
- 結局どうすればいいの?
- (おまけ)ヒューリスティックキャッシュの期間
- We are hiring!
はじめに
こんにちは。プラットフォーム開発部のeinです。
このたび社内でWebアプリケーションを開発するにあたって、今更ながらHTTPキャッシュについて改めて学んだ内容をまとめます。
当記事では特に、Cache-Control
ヘッダおよび ETag
ヘッダについて解説します。
TL;DR
当記事は、MDNのドキュメントを私の理解のもと要約、解説を追加した内容になっています。
https://developer.mozilla.org/ja/docs/Web/HTTP/Caching
キャッシュの種類
大前提として、HTTPキャッシュはおおまかに3種類に分かれます。
- プライベートキャッシュ
- クライアントに保存されるキャッシュ
- 要はブラウザキャッシュのこと
- プライベートキャッシュのみを利用するときは
Cache-Control: private
を用いる
- 共有キャッシュ1
- ヒューリスティックキャッシュ
Cache-Control
が指定されないときの自動的なキャッシュのこと- 制御しにくいため意図的な利用は推奨されない
Cache-Control
によるキャッシュの制御
Cache-Control
レスポンス2ヘッダは、主に キャッシュをするか否か および キャッシュの期間 を制御します。オリジンサーバ3がコンテンツをレスポンスする際に同ヘッダを付与することで、ブラウザやCDNはその内容を解釈してキャッシュの保存期間などを決めます。
ここでは Cache-Control
レスポンスヘッダの主要な値とその意味を解説します。
Cache-Control: max-age
max-age
はキャッシュが新鮮(fresh)である期間を秒単位で指定します。おおむねキャッシュの保存期間と理解して差し支えありません。
例えば Cache-Control: max-age=86400
とすると、ブラウザやCDNはオリジンからレスポンスを受け取った時刻から24時間の間、キャッシュを保存します。
正確には、HTTPキャッシュは古くなっても特定の条件下で再利用し続ける指示も可能であるため、新鮮である期間と保存期間(キャッシュが再利用される期間)は完全には一致しません。
Cache-Control: s-maxage
共有キャッシュに対するmax-ageです。一般にCDNは max-age
と s-maxage
の両方を受け取ったとき、s-maxage
を優先します。
Cache-Control: private
キャッシュをプライベートキャッシュでのみ許可し、共有キャッシュには保存しないよう要求します。
個人にパーソナライズされたコンテンツを共有キャッシュに保存させないために利用します。
Cache-Control: public
逆に、任意のキャッシュに保存してよい旨を明示します。プライベートキャッシュにも共有キャッシュにも保存されます。
共有キャッシュは通常 public
を指定せずともキャッシュしますが、Authorization
ヘッダが付与されいるときなど、コンテンツがパーソナライズされている恐れがあるときにはキャッシュしない実装である場合があります。public
は、そういった制約を無視して共有キャッシュに保存してよいことを明示します。
Cache-Control: no-store
誰もキャッシュしないよう要求します。
Cache-Control: no-cache
ややこしいことに、キャッシュしないという意味ではありません。
ブラウザやCDNがキャッシュを利用する際に、常にキャッシュの 検証 をするよう求めます。検証については後述します。
Cache-Control: immutable
こちらは逆に、キャッシュが新鮮である間は検証しないように求めます。
ブラウザはリロードの際にmax-ageに関わらず検証を実行しますが、immutable
はリロード時の検証すら不要である旨を意味し、キャッシュが新鮮である間はキャッシュを再利用し続けるよう要求します。
ただし immutable
は未対応のブラウザも多いです。
ETag
によるキャッシュの検証
検証は、キャッシュの更新を効率的に行うための仕組みです。
HTTPキャッシュは、古くなっても即時には破棄されません。クライアントはキャッシュが古くなると、コンテンツそのものを要求する前に、サーバーに対してコンテンツが更新されているかどうかを問い合わせます。更新されていなければコンテンツを再取得せずにキャッシュを再利用します。
この問い合わせの仕組みを 検証 と呼び、検証の際にクライアントから送信するリクエストのことを 条件付きリクエスト と呼びます。
検証では主に ETag
レスポンスヘッダおよび If-None-Match
リクエストヘッダを用います4。
ETag
はコンテンツの特定のバージョンを一意に示す値で、一般にハッシュ値やバージョン番号を設定します。クライアントは初回リクエスト時に、コンテンツをキャッシュすると共に ETag
を覚えておきます。そしてクライアントはキャッシュが古くなると検証を実施します。初回リクエスト時に覚えておいた ETag
の値を If-None-Match
に格納してコンテンツをリクエストします(条件付きリクエスト)。サーバは現在のコンテンツの ETag
が If-None-Match
と一致すれば 304 Not Modified
を、不一致であれば 200 OK
および最新のコンテンツをレスポンスします。レスポンスが 304 Not Modified
であった場合は、クライアントは検証時点からさらに max-age
の間、キャッシュを再利用し続けます。
この検証の通信容量はコンテンツそのものよりも大幅に小さいため、max-age
の短いキャッシュであっても、効率的にキャッシュを利用することができます。
結局どうすればいいの?
たくさん説明しましたが、気になるのは「結局どういう場合にどのヘッダを付ければいいの?」だと思います。
以下に、ごく一般的なユースケースと設定を簡単にまとめます。
ETag
は常にレスポンスする- 常に最新のコンテンツを提供したい場合は
Cache-Control: no-cache
を利用する- エンドユーザがアクセスするたびに検証が実行されるため、常に最新のコンテンツを配信できます。
- ほぼ最新のコンテンツを提供したい場合は
Cache-Control: max-age=60
などの短いmax-ageを指定する- 「常に検証してほしい」要件は多くないと思います。たいていのWebサービスでは最長60秒〜数分のキャッシュであれば許容可能でしょう。
- Cache Busting を利用する際には
Cache-Control: max-age=31536000
などの長いmax-ageを指定する- サーバ側から能動的にキャッシュを削除する手法のことを Cache Busting と呼びます。端的には、HTTPキャッシュのキーはURLであることを利用して、キャッシュ削除したいコンテンツのファイル名を変更してしまう手段を取ります。
- Cache Busting を利用する場合には、能動的に削除するまでキャッシュを再利用し続けていてほしいため、長いmax-ageを指定します。
- どうしてもキャッシュされたくない場合は
Cache-Control: no-store
を利用する- 最新版を提供したいユースケースではETagとno-cacheを利用すればよいので、no-storeを利用するケースはセキュリティ要件でどうしてもキャッシュを許容できない場合などに限られるかと思います。
設定例
文章ではイメージが湧きにくいので、例を挙げます。
index.html
にETag
とCache-Control: max-age=60
を設定しますmain.v1.css
にETag
とCache-Control: max-age=31536000
を設定します- 新しいページをリリースする際には、CSSのファイル名を
main.v2.css
に変更して、Cache Busting します
このとき ①初回アクセス ②2回目以降のアクセス ③リリース後のアクセス におけるキャッシュヒットと通信の流れを簡単にシーケンス図を示します。
このように、index.html
にのみ短いキャッシュを設定することで、CSSでは長期間のキャッシュを利用しつつも、リリース後長くとも60秒後には最新版をエンドユーザに届けることができます。
一方、この例では「ETag
があるならCSSも短いキャッシュにしても大差ないのでは?」と思われるかもしれません。ですが、実際のWebシステムではCSSをはじめとするサブリソースの量は膨大になります。0.1秒でも早い読み込みを求められる現代のWebシステムでは、条件付きリクエストすら削減して通信回数を減らす意義は十分にあるはずです。
また、最近のビルドツール、例えばViteなどを利用すればビルド時にファイル名にハッシュ値を付与するのは簡単なので、Cache Busting の実装は比較的容易だと言えるでしょう。
(おまけ)ヒューリスティックキャッシュの期間
みなさまが関わるWebアプリ/サイトの中には、HTTPキャッシュを意識せずに構築されており、Cache-Control
などのHTTPキャッシュを制御するヘッダを一切付与していないものもあるかもしれません。
そのとき気になるのは、ヒューリスティックキャッシュ(自動的なブラウザキャッシュ)の保存期間はどの程度なのか?です。
ヒューリスティックキャッシュの保存期間はRFCに規定はないため、各ブラウザの実装に依存します。具体的な値を調べてみたところ、少なくともChromiumとWebkitにおけるキャッシュ期間は、対象オブジェクトが最後に変更されてから現在までの期間の10% (Dateの値 - Last-Modifiedの値) * 0.1
だそうです5。
例えば、1ヶ月(30日)前に更新されたコンテンツを更新した場合、最長で 2592000秒 * 0.1 = 72時間 の間エンドユーザに最新版が届かないことになります。1年前に更新されたコンテンツであれば 31536000秒 * 0.1 = 36.5日 になりますね。
こうした状態を避けるためにも、あらかじめキャッシュ制御ヘッダを付けておくことはとても重要です。
We are hiring!
みらい翻訳では、私たちと一緒にプロダクト開発を進めてくれるエンジニアを募集しています!ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。
- 共有キャッシュはさらに、プロキシによるプロキシーキャッシュとリバプロやCDNによるマネージドキャッシュに分類されます。↩
-
Cache-Control
ヘッダはリクエストヘッダとしても利用され、レスポンス時とはまた意味が異なります。当記事ではリクエストヘッダとしてのCache-Control
の説明は割愛します。↩ - コンテンツを所持しレスポンスするサーバのこと。↩
-
Last-Modified
ヘッダおよびIf-Modified-Since
ヘッダを用いた検証も可能ですが、当記事では割愛します。↩ - https://paulcalvano.com/2018-03-14-http-heuristic-caching-missing-cache-control-and-expires-headers-explained/↩