HTTP キャッシュ — 同じものを二度運ばない
Cache-Control: max-age で fresh と stale を手計算し、stale になったら ETag と If-None-Match の条件付き GET で確認して 304 なら本文を運ばない — キャッシュ判定の流れをシミュレータ付きで身につける。
更新したのに変わらない・重いページが一瞬で開く — どちらもキャッシュ
CSS を直してアップロードしたのに、ブラウザで見ると古いまま。かと思えば、初回は重かったページが 2 回目は一瞬で開く。良くも悪くも裏にいるのは同じ仕組み — キャッシュです。「同じものを二度運ばない」という Web の速さの心臓部でありながら、仕組みを知らないと「なぜか直らない」の筆頭原因になります。
幸い、HTTP のキャッシュはヘッダの読み方が分かれば怖くありません。判定に登場する問いは 2 つだけです。
Cache-Control: max-ageETagmax-age の手計算 — fresh か stale かは引き算で決まる
サーバはレスポンスにこう添えられます: Cache-Control: max-age=1800。意味は「受け取ってから 1800 秒間は新鮮(fresh)とみなしてよい」です。判定は引き算だけです。
具体的に回してみます。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: 通信ゼロ)、中央の青の出口(304: ヘッダのみ)、右の橙の出口(200: 全転送)— 下のシミュレータでこの 3 つの出口を行き来してみてください。
小問 5-1 — max-age を手計算する
fresh と stale の判定は足し算と引き算だけです。受信時刻・経過秒数・max-age を紙に書いて計算してください。
Q1. レスポンスに Cache-Control: max-age=86400 が付いていました。このキャッシュが fresh でいられる期間は何時間ですか。
max-age の単位は秒です。86400 ÷ 3600 = 24 時間、つまり丸 1 日です。86400 秒 = 1 日はキャッシュ設定の頻出値なので、604800 秒 = 7 日とあわせて手が覚えると設定ファイルがすらすら読めるようになります。
Q2. Cache-Control: max-age=1800 のレスポンスを 10:00:00 に受信しました。10:20:00 の時点で、このキャッシュが fresh でいられる残り時間は何秒ですか。
受信からの経過は 10:00:00 → 10:20:00 で 20 分 = 1200 秒。max-age=1800 秒との差は 1800 − 1200 = 600 秒です。つまり 10:30:00 になった瞬間から stale になります。「経過秒数と max-age を比べるだけ」— fresh 判定の計算はこれがすべてです。
Q3. キャッシュが「stale(新鮮でない)」になったときの正しい理解はどれですか。
stale は「賞味期限切れ」ではなく「要確認」の状態です。キャッシュ自体は手元に残っていて、サーバに条件付きで問い合わせ、「変わっていない」(304)と分かれば引き続き使えます。即削除でも、無確認で使い続けてよいわけでもありません — この「確認してから使う」仕組みが、次に読む条件付き GET です。
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 通りに分かれます。
ここで転送量の感覚を持ってください。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 を送りました。サーバ上のリソースが変わっていなかった場合、サーバの応答として正しいものはどれですか。
If-None-Match は「手元のキャッシュの ETag は abc123 です。これと違うときだけ本文をください」という条件付きの頼みごとです。一致した(変わっていない)なら、サーバは 304 Not Modified を本文なしで返し、ブラウザは手元のキャッシュを使い続けます。200 で全文を返すのは条件が不一致(更新済み)のときで、404 はリソース不存在の別の話です。
Q5. 304 Not Modified が転送量の節約になる理由として正しいものはどれですか。
304 の意味は「あなたの手元にあるものと同じです」— 同じものを運び直す必要がないので、レスポンスはヘッダのみ(高々数百バイト)です。100 KB の本文なら、転送はおよそ数百分の 1 になります。ただしリクエスト自体は毎回飛んでいる点に注意してください。リクエストすら飛ばないのは fresh の間だけで、304 は「飛ぶが、軽い」です。
Q6. Cache-Control: no-store と Cache-Control: no-cache の違いとして正しいものはどれですか。
名前が紛らわしい代表格です。no-store は「そもそも保存するな」(残高など機微な情報向け)、no-cache は「保存してよいが、使う前に毎回確認せよ」(304 が返れば手元のコピーを使える)。厳しいのは no-store のほうです。「no-cache = キャッシュしない」という直感が事故のもとなので、ここで言い分けを固定してください。
no-store と no-cache — 名前にだまされない
最後に、名前が紛らわしいことで有名な 2 つの指示を言い分けます。どちらも Cache-Control に書きますが、意味の強さがまったく違います。
直感に反して、「キャッシュするな」に相当するのは 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 と開発者ツールの出力に、この読み方をそのまま当てはめます。