HTTP キャッシュ

ブラウザ・CDN・プロキシの3層がレスポンスを一時保存する仕組み。 Cache-Control / ETag / stale-while-revalidate を使い分けると 応答時間とサーバ負荷が劇的に下がる。

キャッシュの 3 層

Cache-Control(最重要ヘッダー)

ディレクティブ意味
max-age=NN 秒間 fresh
s-maxage=N共有キャッシュ向けの max-age
no-cache毎回サーバ確認(304 で OK)
no-storeキャッシュしない
privateブラウザのみ。CDN は不可
public共有キャッシュ可
must-revalidate期限切れたら必ず再検証
immutable絶対変わらない(ハッシュ付き URL に最適)
stale-while-revalidate=N古くても N 秒は使い、裏で更新
stale-if-error=Nサーバエラー時 N 秒古くても返す

典型パターン

1. 不変アセット(ハッシュ付き JS / CSS / 画像)

Cache-Control: public, max-age=31536000, immutable

ファイル名にハッシュ(app.a3f2b1.js)が入っていれば1年キャッシュでも安全。 変わったら別 URL になる。

2. HTML(毎回確認)

Cache-Control: no-cache

ETag で 304 が返れば実質キャッシュとして機能。「変わったら即反映」の HTML 向け。

3. API(短期 + SWR)

Cache-Control: public, max-age=60, stale-while-revalidate=300

60 秒間 fresh、その後 300 秒は古いものを返しつつ裏で更新。

4. 個人化されたページ

Cache-Control: private, max-age=0, must-revalidate

5. 絶対キャッシュしない(決済等)

Cache-Control: no-store

ETag

リソースの版識別子。ブラウザが次回 If-None-Match で送り、 サーバが一致したら304 Not Modified を本文なしで返す。

レスポンス:
  ETag: "abc123"
  Cache-Control: max-age=60

次回:
  If-None-Match: "abc123"

→ 一致: 304 Not Modified
→ 違う: 200 OK + 新しい ETag

strong ETag vs weak ETag

Last-Modified(古典)

日付ベースの代替。秒単位の精度なので ETag より弱い。両方付ける運用が普通。

Last-Modified: Wed, 10 May 2026 12:00:00 GMT
↓
If-Modified-Since: Wed, 10 May 2026 12:00:00 GMT

Vary

「このヘッダーで応答が変わるよ」をキャッシュに伝える。

Vary: Accept-Encoding, Accept-Language

✓ 圧縮(gzip / br / 非圧縮)でレスポンスが違うので必須
✓ 認証ありの API は Vary: Authorization
Vary: User-Agent はキャッシュヒット率を下げて避ける

Surrogate-Control(CDN 専用)

Fastly や一部 CDN が見るCDN だけに効くキャッシュヘッダー。 ブラウザは無視するので、CDN を長く・ブラウザを短くできる。

Cache-Control: max-age=60                    (ブラウザ)
Surrogate-Control: max-age=86400             (CDN)

CDN のキャッシュ

Cloudflare / Vercel / Fastly などが各地のエッジでレスポンスをキャッシュ。

パージ

キャッシュキー

既定では URL + Vary。CDN 側でクエリ・Cookie・ヘッダーをキーから外す/入れることで ヒット率を上げられる。

SW Cache(Cache Storage API)

詳細は PWA キャッシュ。 SW で caches.match / caches.put を使う。

fetch のキャッシュモード

SWR(Stale-While-Revalidate)

Cache-Control の stale-while-revalidate ディレクティブ。古くても返して、裏で更新。 UX と新鮮さの折衷。

Cache-Control: max-age=10, stale-while-revalidate=600

SWR を活用する CDN

ブラウザの bfcache

Back-Forward Cache。ページを丸ごと保存して戻る/進むで瞬時復元。 通常のキャッシュとは別レイヤ。

bfcache を有効に保つコツ

Vary と Cookie の悩ましい関係

Vary: Cookie にすると Cookie ごとに別キャッシュ → ヒット率が下がる。
Cookie で出力が変わるのはキャッシュ向きではない。
共通部分をSSR + パブリックキャッシュ、個人化部分をクライアント fetchで取るのが定石。

Service Worker と HTTP キャッシュの優先順位

  1. SW の fetch イベントが先に動く
  2. SW が fetch(req) すると HTTP キャッシュが効く
  3. SW でcache: "no-store" 等を指定可能

キャッシュの落とし穴

推奨デフォルト

✓ 静的ハッシュ付きアセット: max-age=31536000, immutable
✓ HTML: no-cache + ETag
✓ API(公開): max-age=60, stale-while-revalidate=300
✓ 個人化 API: private, no-cache
Vary: Accept-Encoding を必ず付ける