「キャッシュ制御」は“いつまで古いデータを使っていいか”を決めるルール
まずざっくりイメージからいきます。
キャッシュは、「一度取ってきたデータを、次回以降は早く返すために手元に置いておく仕組み」 です。
ブラウザや中継のプロキシサーバーは、
サーバーから受け取ったレスポンスを保存しておいて、
「また同じものが欲しい」と言われたときに、
わざわざサーバーまで取りに行かずに手元から返すことがあります。
これ自体はとても良いことですが、
問題は 「いつまでそれを“新しい”とみなしていいのか」 です。
キャッシュ制御とは、
まさにこの「古いデータをどこまで許すか」を
ヘッダーで決める仕組みです。
キャッシュ制御の主役:Cache-Control ヘッダー
max-age で「何秒間は新鮮か」を決める
一番よく使うのが Cache-Control: max-age=秒数 です。
例えば、サーバーがこう返したとします。
Cache-Control: max-age=60
これは、
「このレスポンスは 60 秒間は新鮮(fresh)とみなしていいよ」
という意味です。
ブラウザはこのレスポンスをキャッシュに保存し、
同じリクエストが 60 秒以内に来たら、
サーバーに行かずにキャッシュから返すことができます。
逆に、60 秒を過ぎると「古い(stale)」とみなされ、
サーバーに取りに行く対象になります。
no-store / no-cache の違い
ここは名前で混乱しやすいところなので、丁寧にいきます。
Cache-Control: no-store
「このレスポンスはどこにも保存するな」
ブラウザもプロキシもキャッシュに残してはいけない。
毎回サーバーに取りに行く。
Cache-Control: no-cache
「キャッシュしてもいいけど、使う前に必ずサーバーに確認しろ」
ブラウザはキャッシュを持つが、再利用前に条件付きリクエストを送る。
つまり、
絶対に保存させたくない → no-store
保存はしていいが、再利用前に必ず確認 → no-cache
という違いです。
パスワードリセットページや、
機密性の高いページは no-store にすることが多いです。
ETag / Last-Modified と「条件付きリクエスト」
「変わってなかったらキャッシュをそのまま使っていい?」と聞きに行く
no-cache のように「確認してから使う」場合、
ブラウザは「条件付きリクエスト」を送ります。
ここで出てくるのが ETag や Last-Modified です。
サーバーがレスポンスにこう付けていたとします。
ETag: "abc123"
Cache-Control: no-cache
ブラウザはこれを覚えておき、
次回同じリソースを取りに行くときにこう聞きます。
If-None-Match: "abc123"
サーバー側は、
「中身変わってないよ」なら 304 Not Modified を返す
「変わったよ」なら 200 OK と新しいボディを返す
という挙動をします。
ブラウザは 304 のとき、
「じゃあ前のキャッシュをそのまま使おう」と判断します。
これが「キャッシュを持ちつつ、古くなっていないか確認する」仕組みです。
fetch とキャッシュ制御の関係
デフォルトでは「ブラウザのキャッシュ戦略」に従う
fetch を何も指定せずに使うと、
基本的にはブラウザの通常のキャッシュルールに従います。
const res = await fetch("/api/data");
JavaScriptこのとき、
サーバーが Cache-Control や ETag をどう返しているか
ブラウザがそのリソースをどうキャッシュしているか
によって、
実際にネットワークに行くか、キャッシュから返るかが決まります。
JavaScript 側からは、
「キャッシュを使うかどうか」をある程度指定することもできます。
fetch の cache オプションで挙動を変える
fetch には cache オプションがあります。
fetch("/api/data", { cache: "no-store" });
JavaScript代表的な値だけ押さえましょう。
default
ブラウザの通常のキャッシュルールに従う(デフォルト)。
no-store
キャッシュを一切使わないし、保存もしない。
毎回ネットワークに行く。
reload
キャッシュを無視してネットワークから取得し、
結果でキャッシュを更新する。
no-cache
キャッシュを使う前に必ずサーバーに確認する(条件付きリクエスト)。
force-cache
可能な限りキャッシュを使う。
なければネットワークに行く。
only-if-cached
キャッシュにある場合だけ使う。
なければエラー(同一オリジン限定)。
初心者としてまず覚えておくといいのは、
「絶対に最新が欲しい」 → cache: "no-store" or cache: "reload"
「普通でいい」 → 何も指定しない(default)
くらいで十分です。
具体例:API のレスポンスをキャッシュさせたくない場合
サーバー側の設定
例えば、ログイン状態や個人情報を返す API で、
ブラウザやプロキシにキャッシュさせたくない場合。
サーバーはこう返します。
Cache-Control: no-store
これで、ブラウザはそのレスポンスをキャッシュに保存しません。
毎回サーバーに取りに行きます。
クライアント側での fetch
クライアント側でも念のためこう書けます。
const res = await fetch("/api/me", {
cache: "no-store",
});
JavaScriptサーバー側の Cache-Control: no-store が本命ですが、
クライアント側でも「キャッシュを使わない」と明示しておくと安心です。
ここでのポイントは、
「本当にキャッシュさせたくないかどうかは、サーバー側が決める」
ということです。
fetch の cache オプションは「ブラウザに対するお願い」に近く、
最終的な権限はレスポンスヘッダー側にあります。
具体例:静的ファイルをガッツリキャッシュさせたい場合
ビルド済みの JS / CSS など
SPA などで、ビルド済みの JS / CSS ファイルに
ハッシュ付きのファイル名を付けている場合(例: app.abc123.js)、
そのファイルは「内容が変わったらファイル名も変わる」前提です。
この場合、サーバーはこう返せます。
Cache-Control: max-age=31536000, immutable
max-age=31536000 は 1 年immutable は「この URL の中身は変わらない」と宣言するもの
ブラウザは一度取ってきたら、
ほぼ二度とネットワークに行かずキャッシュから返します。
新しいビルドが出たら、
ファイル名が app.def456.js のように変わるので、
そのときだけ新しいファイルを取りに行きます。
これが「キャッシュを前提にしたフロントエンドの定番パターン」です。
初心者としてキャッシュ制御で本当に掴んでほしいこと
キャッシュ制御は「いつまで古いデータを使っていいか」を決めるルール
主役は Cache-Control ヘッダー(max-age / no-store / no-cache など)no-store は「保存禁止」、no-cache は「使う前に確認」ETag や Last-Modified と If-None-Match / If-Modified-Since で「変わったかどうか」を確認できるfetch の cache オプションで、ある程度ブラウザのキャッシュの使い方を指定できる
「絶対に最新が欲しい API」と「ガッツリキャッシュしていい静的ファイル」を意識的に分ける
一番いい練習は、
ブラウザの DevTools の「Network」タブを開いて、
同じリソースを何度かリロードしてみる
ステータスが「200」から「304」になったり、「from disk cache」になったりするのを見る
レスポンスヘッダーの Cache-Control / ETag を眺める
ことです。
キャッシュ制御は、
「なんとなく速くなっている裏側の仕組み」ではなく、
「あなたが設計できるパフォーマンスの武器」 です。
そこに気づけると、ネットワーク周りの設計が一段楽しくなります。
