はじめに
渋川さんのReal World Httpのキャッシュの章を読んだので自分なりに整理してみました。
1. そもそもキャッシュとは?
Webブラウザは、一度取得したHTML、CSS、JavaScript、画像などのリソースを保存しておき、次回以降のアクセスで再利用できます。
これがHTTPキャッシュです。
キャッシュを利用することで、
- 通信量を減らす
- ページの表示を高速化する
- サーバーへのリクエストや負荷を減らす
といったメリットがあります。
理解のポイントは、「キャッシュする / キャッシュしない」の二択ではなく、次の2つの判断です。
1. サーバーに問い合わせず、手元のキャッシュをそのまま利用してよいか
2. サーバーに問い合わせたうえで、手元のキャッシュを再利用してよいか
この違いが、`Expires`、`Cache-Control`、`Last-Modified`、`ETag`の役割に対応します。
2. 一つの例で流れを見る
次のレスポンスを例にします。
```http
HTTP/1.1 200 OK
Cache-Control: max-age=3600
ETag: "abc123"
```
`max-age=3600`は、この応答を保存してから3600秒(1時間)は新鮮(fresh)として扱う、という指定です。
`ETag`は、そのコンテンツにつけた識別子です。
初回アクセス
ブラウザはコンテンツと、これらのヘッダを保存します。
freshのあいだ
1時間以内の再アクセスでは、キャッシュはfreshです。
通常のページ遷移では、サーバーへ問い合わせず手元のキャッシュを使います。
再読み込みでは、freshでも条件付きリクエストを送るブラウザがあります。
staleになったあと
1時間を過ぎるとキャッシュはstaleになります。
staleは「捨てる」ではありません。サーバーへ再検証し、変わっていなければ再利用できます。
再検証では、保存していたETagを送ります。
```http
If-None-Match: "abc123"
```
サーバー側のETagが同じなら`304 Not Modified`が返ります。
304にコンテンツの本体は含まれません。ブラウザは保存済みのキャッシュを再利用します。
中身が変わっていれば、新しいETagとともに`200 OK`と本体が返ります。ブラウザはキャッシュを置き換えます。
```text
キャッシュはまだfresh?
│
├─ YES
│ サーバーへ行かず、キャッシュを使う
│
└─ NO(stale)
↓
If-None-Match で確認
│
├─ 一致 → 304。キャッシュを再利用
│
└─ 不一致 → 200 + 新しいコンテンツ
```
`If-None-Match`を送るのは、この再検証のときです。freshなあいだの通常遷移では、リクエスト自体が飛びません。
役割は分かれています。
```text
max-age
→ いつまでサーバーへ確認せず使えるか
ETag
→ 確認したとき、手元のキャッシュを再利用できるか
```
この2つは競合しません。期限と、期限後に取り直すかの判定を組み合わせて使います。
---
3. 鮮度を決める
freshのあいだは、サーバーへ問い合わせずにキャッシュを使えます。その期限を書くのが鮮度のヘッダです。
Expires
`Expires`は、いつまでfreshと扱うかを絶対日時で指定します。HTTP/1.0からあるヘッダです。
```http
Expires: Thu, 24 Sep 2026 08:00:00 GMT
```
この日時より前なら、ブラウザはサーバーへ問い合わせずキャッシュを使えます。
クライアントとサーバーの時計がずれていると、意図より早く、または遅く期限切れになります。
max-age
HTTP/1.1の`Cache-Control`では、同じことを受け取ってからの秒数で指定できます。
```http
Cache-Control: max-age=86400
```
`86400`秒なので、24時間freshです。
`Expires`と`max-age`が両方あるときは、`max-age`が優先されます。
4. 再検証する
staleなキャッシュを再利用するときは、サーバーに「前回から変わったか」を尋ねます。
変わっていなければ304、変わっていれば200です。
この問い合わせに使うのがバリデータです。
Last-Modified
サーバーは更新日時を返せます。こちらもHTTP/1.0からあります。
```http
Last-Modified: Wed, 23 Sep 2026 08:00:00 GMT
```
再検証するとき、ブラウザはこの日時を送ります。
```http
If-Modified-Since: Wed, 23 Sep 2026 08:00:00 GMT
```
その日時よりあとで更新されていれば200、そうでなければ304です。
毎回のアクセスで送るヘッダではありません。freshなら送りません。
`Expires`も`max-age`もない応答では、キャッシュが`Last-Modified`から鮮度を推測することがあります。更新日時は再検証の材料であると同時に、この推測にも使われます。
ETag
HTTP/1.1では`ETag`が追加されました。更新日時ではなく、サーバーが発行した識別子で変更を判断します。
```http
ETag: "abc123"
```
再検証時のヘッダは`If-None-Match`です。
`Last-Modified`は秒単位です。同じ秒に二度変わると区別できず、中身が同じでも日時だけ進むと200になります。
ETagは内容から識別子を作ることもできます。
5. 保存の範囲と、使う前の確認
`Cache-Control`には、鮮度以外の指示もあります。カンマ区切りで複数書けます。
ここでの共有キャッシュは、CDNやプロキシのように複数ユーザーで使い回すキャッシュです。ブラウザのキャッシュは、そのユーザーだけのものです。
no-cache
```http
Cache-Control: no-cache
```
名前は「キャッシュしない」に見えます。意味は、保存したうえで、使う前に必ず再検証する、です。
`ETag`か`Last-Modified`があれば304になり得ます。
バリデータがないと304にできず、毎回200で本体が返ります。
```text
no-cache
→ 保存できる
→ 利用前にサーバーへ確認する
```
no-store
保存自体をさせない指定です。
```http
Cache-Control: no-store
```
```text
no-store
→ 保存しない
```
private
共有キャッシュには保存させず、ブラウザには保存できる、という指定です。
```http
Cache-Control: private
```
ユーザーごとに内容が違う応答に使います。共有キャッシュがユーザー別に持ち分ける、という意味ではありません。共有キャッシュは保存しません。
public
通常なら共有キャッシュが保存しない応答でも、保存してよいと明示する指定です。
`Authorization`付きリクエストへの応答が、その典型です。
```http
Cache-Control: public
```
`Authorization`が付いていない通常のGETの200は、`public`がなくても共有キャッシュに保存され得ます。
続く二つは、freshのあいだと、staleになった直後の動きを足す拡張です。HTTP/1.1当初のディレクティブではありません。
immutable
freshな期間中、その表現は変わらない、という宣言です。
```http
Cache-Control: max-age=31536000, immutable
```
これを付けると、再読み込みでもfreshのあいだは再検証を省略できます。
ファイル名に内容のハッシュが入った静的ファイルと一緒に使います。
stale-while-revalidate
staleになった直後に限り、古い応答を先に返しながら、裏で再検証します。
```http
Cache-Control: max-age=60, stale-while-revalidate=300
```
この例では、最初の60秒はfreshです。
その後300秒は、古いキャッシュをすぐに表示し、バックグラウンドで更新します。
それを過ぎると、再検証が終わるまで古い応答は使えません。
6. 実際にはどのように設定する?
コンテンツの性質でヘッダを変えます。
HTML
HTMLは更新されうるので、毎回再検証させることが多いです。
```http
Cache-Control: no-cache
ETag: "abc123"
```
`ETag`か`Last-Modified`を一緒に返すと、変わっていないとき304になります。
バリデータがないと、毎回本体付きの200になります。
ユーザー固有のレスポンス
ユーザーごとに内容が違うGETなら、共有キャッシュには載せず、ブラウザでは再検証して使います。
```http
Cache-Control: private, no-cache
```
ブラウザには保存されます。ディスクに残したくない機密なら`no-store`にします。
JavaScript / CSS / 画像
Viteなどのビルドツールは、ファイル名に内容のハッシュを入れることがあります。
```text
/assets/index.a8f31c.js
```
内容が変わるとURL自体が変わります。古いURLの中身が後から差し替わることはありません。
そのため、長いfreshと`immutable`を付けられます。
```http
Cache-Control: max-age=31536000, immutable
```
1年をfreshとし、そのあいだ変わらないと宣言できます。
---
7. まとめ
HTTPキャッシュには、大きく分けて2つの判断があります。
```text
Cache-Control / Expires
↓
今持っているキャッシュを
サーバーへ問い合わせず使ってよいか?
```
そして、
```text
ETag / Last-Modified
↓
サーバーへ問い合わせた結果
今持っているキャッシュを再利用してよいか?
```
変更されていなければ、
```http
304 Not Modified
```
が返り、コンテンツ本体を再ダウンロードすることなく手元のキャッシュを利用できます。
特に覚えておきたいのが、
```text
no-cache
→ キャッシュしない、ではない
→ 再利用前に再検証する
no-store
→ キャッシュを保存しない
max-age
→ キャッシュをサーバーへ問い合わせず使える期間
ETag / Last-Modified
→ staleになったキャッシュを再利用できるか確認する仕組み
```
という違いです。
HTTPキャッシュを単純な「保存する / 保存しない」という仕組みとして考えるのではなく、
**「いつまでそのまま使えるのか」と「古くなったあと、どうやって再利用するのか」**
という2段階で考えると、HTTPのキャッシュ制御を整理しやすくなります。