合同会社小村ソフト
第 5 章

HTTP キャッシュ — 同じものを二度運ばない

Cache-Control: max-age で fresh と stale を手計算し、stale になったら ETag と If-None-Match の条件付き GET で確認して 304 なら本文を運ばない — キャッシュ判定の流れをシミュレータ付きで身につける。

更新したのに変わらない・重いページが一瞬で開く — どちらもキャッシュ

CSS を直してアップロードしたのに、ブラウザで見ると古いまま。かと思えば、初回は重かったページが 2 回目は一瞬で開く。良くも悪くも裏にいるのは同じ仕組み — キャッシュです。「同じものを二度運ばない」という Web の速さの心臓部でありながら、仕組みを知らないと「なぜか直らない」の筆頭原因になります。

幸い、HTTP のキャッシュはヘッダの読み方が分かれば怖くありません。判定に登場する問いは 2 つだけです。

問い 1 — まだ新鮮(fresh)か?
新鮮な間は、リクエストすら送らず手元のコピーを使う。新鮮さの期限を決めるのが Cache-Control: max-age
問い 2 — 期限が切れた(stale)なら、中身は変わったか?
サーバに条件付きで確認し、変わっていなければ「そのまま使え」(304)、変わっていれば新品(200)が届く。確認の目印が ETag

max-age の手計算 — fresh か stale かは引き算で決まる

サーバはレスポンスにこう添えられます: Cache-Control: max-age=1800。意味は「受け取ってから 1800 秒間は新鮮(fresh)とみなしてよい」です。判定は引き算だけです。

経過時間 < max-age → fresh
リクエストを送らず、手元のキャッシュを即座に使う。通信ゼロ、最速
経過時間 ≥ max-age → stale
「要確認」の状態。捨てるのではなく、サーバに確認してから使う

具体的に回してみます。10:00:00 に max-age=1800(= 30 分)のレスポンスを受信したとすると、10:20:00 時点の経過は 1200 秒。1200 < 1800 なのでまだ fresh、残りは 600 秒です。10:30:00 を過ぎた瞬間から stale になります。よく見る値の換算も手に馴染ませておきましょう — max-age=86400 は 1 日(86400 = 24 × 3600)、max-age=604800 は 7 日、max-age=31536000 は 365 日です。

大事な線引きをもう一つ。stale = 使用禁止ではありません。stale は「確認せずには使えない」であって、確認して変わっていなければ使い続けられます。この「確認」の仕組みが次の主役、条件付き GET です。

キャッシュの有無、fresh か stale か、ETag の有無で分岐し、リクエストなし・304・200 のいずれかに至る HTTP キャッシュ判定のフローチャート

キャッシュ判定の全体フローです。左の緑の出口(fresh: 通信ゼロ)、中央の青の出口(304: ヘッダのみ)、右の橙の出口(200: 全転送)— 下のシミュレータでこの 3 つの出口を行き来してみてください。

小問 5-1 — max-age を手計算する

fresh と stale の判定は足し算と引き算だけです。受信時刻・経過秒数・max-age を紙に書いて計算してください。

Q1. レスポンスに Cache-Control: max-age=86400 が付いていました。このキャッシュが fresh でいられる期間は何時間ですか。

時間

Q2. Cache-Control: max-age=1800 のレスポンスを 10:00:00 に受信しました。10:20:00 の時点で、このキャッシュが fresh でいられる残り時間は何秒ですか。

Q3. キャッシュが「stale(新鮮でない)」になったときの正しい理解はどれですか。

ETag と条件付き GET — 「変わっていたときだけください」

stale になったキャッシュの確認は、こんな 2 段構えで行われます。まず最初のレスポンスで、サーバは本文の「指紋」を添えておきます。

HTTP/1.1 200 OK
Cache-Control: max-age=1800
ETag: "abc123"
Content-Length: 102400

ETag は、その時点の本文の版を表す短い識別子です。期限が切れた後、ブラウザは普通の GET ではなく条件付き GET を送ります。

GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abc123"

If-None-Match は「手元の版は abc123 です。これと違うときだけ本文をください」という条件の添え書きです。サーバの答えは 2 通りに分かれます。

変わっていない → 304 Not Modified
本文なし・ヘッダのみの返事。「手元のものをそのまま使ってよい」。100 KB の本文でも転送は数百バイトで済む
変わっていた → 200 OK
新しい本文と新しい ETag が届く。キャッシュを差し替える

ここで転送量の感覚を持ってください。fresh はリクエストすら飛ばない(0 バイト)。304 はリクエストは飛ぶがヘッダだけ(数百バイト)。200 は全部(本文まるごと)。キャッシュ設計とは、この 3 つの出口への振り分けを設計することです。

シミュレータで確かめる

下のシミュレータは、1 つのファイル(本文 120 KB と仮定)に対するキャッシュ判定を再現します。max-age と経過時間のスライダー、ETag の有無とサーバ側の更新の有無のトグルを動かして、リクエストは飛ぶか・飛ぶなら条件付きか・レスポンスは 200 か 304 か・転送量はいくらかが切り替わる様子を確かめてください。

シミュレータの動作概要(テキスト版): max-age スライダーと経過時間スライダーを動かすと、経過時間が max-age 未満なら「fresh — リクエストなし・転送量 0 KB」と判定されます。経過時間が max-age 以上(stale)のとき、ETag トグルがオフなら「通常の GET → 200 → 約 120.3 KB 転送」、ETag トグルがオンなら「If-None-Match 付きの条件付き GET」となり、「サーバ上で更新があった」トグルがオフなら「304 → 約 0.3 KB 転送」、オンなら「200 → 約 120.3 KB 転送」と表示されます。判定の理由も 1 行ずつテキストで表示されます。

小問 5-2 — 条件付き GET と保存の禁止

ETag・If-None-Match・304 の連携と、no-store / no-cache の言い分けです。

Q4. stale になったキャッシュについて、ブラウザが If-None-Match: W/"abc123" を付けた GET を送りました。サーバ上のリソースが変わっていなかった場合、サーバの応答として正しいものはどれですか。

Q5. 304 Not Modified が転送量の節約になる理由として正しいものはどれですか。

Q6. Cache-Control: no-storeCache-Control: no-cache の違いとして正しいものはどれですか。

no-store と no-cache — 名前にだまされない

最後に、名前が紛らわしいことで有名な 2 つの指示を言い分けます。どちらも Cache-Control に書きますが、意味の強さがまったく違います。

no-store — 保存するな
キャッシュへの保存自体を禁止。口座残高や個人情報など、コピーがどこにも残ってほしくないレスポンスに使う。最も厳しい
no-cache — 保存してよいが、毎回確認せよ
保存は許すが、使う前に毎回サーバへの再検証を必須にする。ETag が一致して 304 が返れば手元のコピーを使える。「max-age=0 で常に要確認」に近い挙動

直感に反して、「キャッシュするな」に相当するのは no-cache ではなく no-store です。no-cache は「キャッシュを使う前に必ず確認せよ」であり、304 と組み合わせれば転送量は節約されます。この取り違えは実務で本当によく見かけます — 「no-cache を付けたのに端末にコピーが残っている」は、仕様どおりの動作です。

この章で持ち帰ること

  • キャッシュ判定の問いは 2 つ — まだ fresh か(max-age との引き算)、stale なら中身は変わったか(条件付き GET)
  • fresh の間はリクエストすら飛ばない。stale は「使用禁止」ではなく「要確認
  • ETag + If-None-Match → 変わっていなければ 304(本文なし・ヘッダのみ)。出口は 3 つ: 0 バイト / 数百バイト / 全転送
  • no-store は保存禁止、no-cache は毎回再検証。「キャッシュするな」は no-store のほう

ここまでで 3 点セットの読み方が一通りそろいました。次章はいよいよ実物です — curl と開発者ツールの出力に、この読み方をそのまま当てはめます。