前回の記事(Qiita)でフロントエンドにおけるキャッシュの各レベルを俯瞰しました:フロントエンドにおけるキャッシュの全レベルを理解する。
本記事は同じシリーズの続きで、ブラウザが自動的に処理する HTTP Cache の仕組みと、基本的な動きを整理します。
1. HTTP Cache とは何か
HTTP Cache(Browser Cache)は、ブラウザが一度取得したリソース(HTML / CSS / JavaScript / 画像 / フォントなど)をローカルに保存し、再利用する仕組みです。
同じリソースに再度アクセスするとき、条件が満たされていれば、ブラウザは サーバーへリクエストを送らず、キャッシュからすぐ応答できます。
これにより:
- ページ表示が速くなる
- ネットワークリクエストが減る
- 帯域を節約できる
HTTP Cache は特定の API ではなく、HTTP の仕様の一部として定義されており、主に次のヘッダーで制御されます:
Cache-ControlETagLast-Modified
2. 動作の基本フロー
リソースが必要になると、ブラウザはおおよそ次のように振る舞います:
Request → キャッシュ確認
→ Fresh → キャッシュを使う
→ Stale → 再検証(revalidation)
もう少し詳しく:
[リクエスト発生]
↓
キャッシュ確認
↓
キャッシュにある?
┌────┴────┐
ある ない
│ │
↓ ↓
Fresh? サーバーへリクエスト
│
┌─┴──┐
はい いいえ
│ │
↓ ↓
使用 再検証(ETag / Last-Modified)
↓
304 または 200
Fresh と Stale
-
Fresh:
max-ageの範囲内(など)→ 多くの場合 そのままキャッシュを使い、ボディを取り直さない - Stale:鮮度が切れた → サーバーに確認(304 または 200 + 新しいボディ)
補足: ブラウザは無駄なダウンロードを減らすように最適化されていますが、細かい挙動は ヘッダー、HTTP メソッド(GET/POST など)、実装 によって変わります。
3. 主要な HTTP ヘッダー
3.1 Cache-Control
キャッシュの振る舞いを決める、いちばん重要なヘッダーです。
Cache-Control: public, max-age=3600
→ 他のディレクティブと矛盾しない範囲で、3600 秒(1 時間) fresh とみなされやすい、というイメージです。
よく出てくるディレクティブ
| ディレクティブ | 意味(要約) |
|---|---|
max-age |
レスポンスが fresh とみなされる秒数 |
no-cache |
保存され得るが、使う前に オリジンで再検証が必要(検証なしでキャッシュを使えない) |
no-store |
保存しない(保存してはならない) |
public |
共有キャッシュ(CDN など)と プライベートキャッシュ(ブラウザ)の両方でキャッシュされ得る(他条件も満たす場合) |
private |
主に プライベートキャッシュ(多くはブラウザ)。共有キャッシュが他ユーザー向けに保持・再利用しない意図 |
immutable |
そのエントリの寿命中は内容が変わらないというヒント。fresh の間は追加の再検証を避けやすい |
stale-while-revalidate |
stale になったあと、一定時間は 古いレスポンスを返しつつ バックグラウンドで更新し得る |
no-cache と no-store の違い
Cache-Control: no-cache
- レスポンスは キャッシュに保存され得る
- ただし 使う前にサーバーで有効性を確認する(
If-None-Match/If-Modified-Sinceなど)
Cache-Control: no-store
- 保存しない(または保存してはならない)
→ no-cache は名前のせいで「キャッシュしない」と誤解されがちですが、実際は 「検証なしでは使わない」 に近い理解が安全です。
3.2 ETag
リソースの版を表す識別子(多くはハッシュやサーバーが付与する値)です。
再検証するときにクライアントが送ります:
If-None-Match: "abc123"
サーバー:
- 内容が ETag と一致 → 多くは 304 Not Modified(ボディなしになりやすい)
- 変わっている → 200 と新しいボディ(と新しい
ETagなど)
3.3 Last-Modified
最終更新日時に基づく検証です。
If-Modified-Since: <date>
注意点:
- 精度は多くの場合 秒 まで
- ビルドの過程で、内容は同じでもタイムスタンプだけずれる(逆もあり得る)ため、時刻だけに頼ると注意が必要
4. よくあるパターン
4.1 静的アセット(ファイル名にハッシュ)
Cache-Control: public, max-age=31536000, immutable
例:app.abc123.js
流れ:
- 初回 → 200 で取得
- その後(fresh の間)→ キャッシュから。ネットワークリクエストは不要になりやすい
- 新しい版が出ると ファイル名(ハッシュ)が変わる → URL が変わり → 新しいファイルを取得
特徴:
- ファイル名が変わらないアセットより再検証が少なくて済みやすい
- ブラウザと CDN の両方で効きやすい定番パターン
4.2 バージョンを付けない HTML
Cache-Control: no-cache
ETag: "html-123"
流れ:
- 初回 → 200(キャッシュに保存され得る)
- 次回 →
If-None-Matchなどで検証 - サーバー:変化なし → 304、変化あり → 200 と新しい HTML
特徴:
- 毎回サーバーに確認してからキャッシュを使うため、検証なしで古い HTML を返し続けるリスクを抑えやすい
-
304 なら変更がないとき ボディを再送しない で済む
※「常に最新が保証される」とまでは言えません。検証した時点でサーバーと整合している、という意味にとどまります。
4.3 あまり変わらない API(ユーザーに依存しないデータ)
Cache-Control: public, max-age=60
流れ:
- 60 秒以内 → キャッシュの利用があり得る(メソッドやステータスなど HTTP の条件も満たす場合)
- それ以降 → stale → 新しいリクエストや再検証
トレードオフ:
- サーバー負荷を下げやすい
- 画面に出るデータは最大で
max-age分 遅れる可能性がある(別の仕組みがなければ)
4.4 ユーザーごとに違う API
Cache-Control: private, max-age=60
- 主に そのユーザーのブラウザ にキャッシュされやすい
- 共有キャッシュ(多くのユーザー向けの CDN など)が、他ユーザーにこのレスポンスを返さない意図
→ ユーザー間でデータが取り違えられるリスクを下げるためのよくある設定です。
4.5 リアルタイム性が高い/機密の API
Cache-Control: no-store
- ストレージ型のキャッシュには載せない(または載せないようにする)
- 必要なたびにサーバーへ問い合わせやすい(アプリ内のメモリなどは別レイヤー)
4.6 stale-while-revalidate
Cache-Control: public, max-age=60, stale-while-revalidate=300
イメージ(stale になったあと、stale-while-revalidate の秒数の範囲内):
- すぐ キャッシュ上の(やや古い)レスポンスを返し得る
- 同時に バックグラウンドで新しい取得・再検証が走り得る
- あとのリクエストでは更新後のデータが見えやすいが、バックグラウンド取得の完了タイミング次第で 「次のリクエストで必ず新しい」とは限らない
補足: 挙動はブラウザやリソースの種類で違い得ます。確実にしたいときはブラウザのドキュメントを読み、実際に試すとよいでしょう。
4.7 stale-if-error
Cache-Control: public, max-age=60, stale-if-error=300
イメージ:
- 取得や再検証が エラーになったとき、一定時間 stale を返し得る
重要: このディレクティブは CDN や中間のサーバーでよく使われます。一方、ブラウザの HTTP キャッシュ では必ずしも同じようには動きません。入門としては「主に コンテンツ配信ネットワーク(CDN) 側の話で、ブラウザでは同じ挙動を期待しすぎない」と覚えておけば十分です。
5. Validation の仕組み
エントリが fresh でなくなったとき、または no-cache などの条件のとき、クライアントは例えば次を送ります:
If-None-Match
If-Modified-Since
HTTP の仕様上、多くのサーバーでは両方ある場合 ETag(If-None-Match)の照合を優先しやすいです。
レスポンス:
- 304 → すでにあるボディを再利用(ヘッダーだけ更新されることも)
- 200 → 新しいボディ(と新しい validator が付くことも)
6. Express を使った簡単な例
以下は 最小のサンプル で、レスポンスにヘッダーがどう付くかを見るためのものです。
メモ: 静的ファイル(ハッシュ付き JS/CSS や画像など)のキャッシュは、Nginx や Apache、CDN 側で設定するプロジェクトも多く、アプリのコードだけで扱うとは限りません。下の Express の例は ヘッダーの意味に慣れる ためのもので、同じ方針をあとから Web サーバーの設定に写し替えるイメージで読んでください。
6.1 静的ファイル
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true
}));
よくある結果:
- 初回 → 200
- その後(fresh の間)→ キャッシュから。ネットワークリクエストは不要になりやすい
6.2 HTML
app.get('/', (req, res) => {
res.set('Cache-Control', 'no-cache');
res.sendFile('index.html');
});
よくある結果:
- 初回 → 200
- 次回以降、Express が ETag を付け、ファイル内容が変わっていなければ 304 になり得る(
etagの設定次第)
6.3 ETag 付き API(サンプル)
const crypto = require('crypto');
app.get('/api/data', (req, res) => {
const data = JSON.stringify({ value: 123 });
const etag = crypto.createHash('md5').update(data).digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.set('ETag', etag);
res.set('Cache-Control', 'no-cache');
res.send(data);
});
意図:
- 変化なし → 304
- 変化あり → 200
7. よくある間違い
-
Cache-Controlを付けていない=キャッシュされない と思うこと
→ 付けなくても ヒューリスティック などでキャッシュされ得ます。意図を決めたうえで ヘッダーを明示するのが安全です。 -
no-cacheを「保存しない」と誤解すること - ハッシュのない URL のアセットに長期キャッシュを載せること → ファイルを更新したあと、ブラウザに新しい版を取らせにくい
- ユーザー固有・機密のレスポンスに
publicを付けること
補足:Vary ヘッダー
同じ URL でも Accept-Encoding や Accept-Language、Cookie などで レスポンスが変わる場合、Vary が キャッシュキー に効きます。Vary が不適切だと 別の表現(gzip と非 gzip など)を取り違えることがあります。ここは一段上の話題なので、上の内容に慣れてから読み進めるとよいです。
8. まとめ
ブラウザの HTTP Cache は:
- ヘッダーを置くだけで効かせやすい一方、意味を取り違えるとハマりやすい
押さえるとよいのは次の 3 点です:
- 何をキャッシュしてよいか
- どれくらい fresh とみなすか/いつサーバーに確認するか
- 誰がコピーを持ってよいか(ブラウザ / CDN / プロキシ)