「キャッシュ」と聞いて、ブラウザの強制再読み込みを思い浮かべる人もいれば、CDNやRedisを思い浮かべる人もいる。どれも同じキャッシュという言葉で呼ばれるが、置かれている場所も、保持しているものも、減らしたいコストも違う。
たとえば、あるAPIのデータを画面に表示するとする。クライアント側のデータキャッシュに残っていればAPIへ通信しない。通信してもCDNで応答できればOriginには届かない。Originまで届いてもRedisに結果が残っていればDBへの問い合わせを避けられる。DBへ問い合わせたとしても、必要なデータページがメモリ上にあればストレージI/Oは発生しないことがある。
つまり、Webサービスでは「キャッシュを使っているか」よりも、「どの経路の、どのコストをキャッシュで省いているか」と考えた方が実態に近い。
キャッシュという仕組み自体は新しいものではない。HTTPキャッシュもDNSキャッシュも、プロキシキャッシュも長く使われてきた。一方で、CDNやEdge、Service Worker、クライアント側のデータ取得ライブラリ、分散キャッシュなどが同じサービスの中で重なることは珍しくなくなった。まずは個々の製品名から離れ、データがユーザーへ届く経路として見てみる。
職種ではなく、リクエストの経路で見る
フロントエンド、サーバーサイド、クラウドと職種ごとにキャッシュを分けることもできる。ただ、実際の境界はそれほど明確ではない。
フロントエンドエンジニアが Cache-Control を設定してCDNの挙動に関わることもあれば、サーバーサイドエンジニアがRedisとCDNの両方を見ることもある。Next.jsのようにクライアントとサーバーの両方を扱うフレームワークでは、職種名だけで分類するとかえって分かりにくい。
Webの経路を大きく分けると、次のように考えられる。
flowchart TD
A[ブラウザ / アプリ] --> B[CDN / Edge]
B --> C[Web / API]
C --> D[Application Cache]
D --> E[Database]
E --> F[OS / Storage]DNSはこのHTTP通信の途中にレスポンスを中継するものではなく、接続先を決める名前解決側の経路にある。
flowchart TD
A[ブラウザ / アプリ] --> B[OS / ローカルの名前解決]
B --> C[Recursive Resolver]
C --> D[権威DNS]実際の構成はサービスごとに異なる。CDNを使わない構成もあれば、CDNの中に複数段のキャッシュを持つ構成もある。アプリケーションキャッシュとしてRedisを置かず、プロセス内メモリだけを使う場合もある。
サービスごとに段数は違っても、キャッシュは複数のスコープに存在できる。同じデータが経路上で何度もキャッシュされる場合もある。
| スコープ | 代表例 | 主に保持するもの | 主に減らすもの |
|---|---|---|---|
| クライアント | HTTP Cache、Cache Storage、データ取得ライブラリのキャッシュ | HTML、CSS、JS、画像、APIデータ | 待ち時間、通信量、APIリクエスト |
| 名前解決 | OS、ブラウザ、Recursive ResolverのDNSキャッシュ | DNSレコード | DNS問い合わせ、名前解決の待ち時間 |
| 配信 | CDN、Edge Cache、Tiered Cache | HTTPレスポンス、静的アセット | Originへのリクエスト、待ち時間 |
| アプリケーション | プロセス内メモリ、Redis、Reverse Proxy Cache | API結果、計算結果、参照頻度の高いデータ | DBアクセス、計算量、応答時間 |
| データベース | Buffer Pool、共有バッファ | データページ、インデックスページ | ストレージI/O |
| OS・ストレージ | Page Cache、ストレージ内部のキャッシュ | ファイルやブロック | 物理I/O |
クライアント側には「HTTPのキャッシュ」と「アプリのデータキャッシュ」がある
ブラウザ側だけを見ても、キャッシュは1種類ではない。
HTTP Cache
CSS、JavaScript、画像、HTMLなどのHTTPレスポンスは、Cache-Control や ETag、Last-Modified などのHTTPヘッダーに従って再利用できる。
たとえば、まだfreshなレスポンスであればネットワークへ問い合わせず、そのままキャッシュを利用できる。期限が切れて再検証が必要な場合でも、ETag や Last-Modified を使えば、内容が変わっていないことを確認してレスポンス本文の再転送を避けられる場合がある。
ここでは「リクエスト自体を発生させないキャッシュ」と「問い合わせはするが転送量を減らす再検証」を分けて考える必要がある。
ブラウザの開発者ツールではmemory cacheやdisk cacheと表示されることがあるが、アプリケーション側から通常意識するのはHTTPキャッシュの規則だ。実際にメモリへ置くかディスクへ置くかといった内部実装は、ブラウザ側の判断も大きい。
クライアントのデータキャッシュ
SWRやTanStack Queryのようなデータ取得ライブラリも、APIから取得した値をクライアント側で保持する。
これはHTTP Cacheとは別の層だ。URL単位のHTTPレスポンスではなく、query keyなどアプリケーション側の単位でデータを管理し、一定時間は再取得しない、画面へ前回値をすぐ表示して裏で再取得する、といった制御ができる。
このキャッシュが効けば、コンポーネントが再描画されたり画面を移動したりしても、同じAPIを毎回呼ぶ必要がない。APIリクエスト回数そのものを減らす目的が分かりやすく現れる層だ。
Service WorkerとCache Storage
Service Workerはネットワークリクエストを横取りし、Cache Storageなどを使ってアプリ側のルールでレスポンスを保持できる。
HTTP Cacheよりも明示的な制御ができ、オフライン対応や「キャッシュを先に返して裏で更新する」といった構成にも使われる。その分、どのタイミングで更新するか、古いキャッシュをどう扱うかもアプリ側の設計に入ってくる。
ネイティブアプリでも考え方は近く、メモリキャッシュ、HTTPクライアントのキャッシュ、ファイル、SQLiteなどのローカルデータがクライアント側の層になる。Webと比べると、オフライン利用や長期保存を前提にローカルデータストアを設計する場面が多い。
DNSもキャッシュされる
example.com へアクセスするとき、毎回権威DNSサーバーへ問い合わせてIPアドレスを取得しているわけではない。OS側の名前解決処理やRecursive Resolverなどの途中でDNSレコードがキャッシュされる。
DNSレコードにはTTLがあり、その値を基準に一定期間再利用される。実際にはブラウザ、OS、アプリケーションランタイム、利用しているResolverなどがそれぞれ名前解決結果を保持することもあるため、見えている層は1つとは限らない。
DNSキャッシュが減らしているのは、Web APIへのリクエストではなく名前解決の問い合わせだ。HTTPレスポンスのキャッシュとは別物だが、「同じ問い合わせ結果を一定期間使い回す」という意味では同じキャッシュの考え方にある。
CDNとEdge CacheはOriginまで届くリクエストを減らす
CDNは、ユーザーに近い拠点へHTTPレスポンスを保持し、Originへ毎回アクセスしなくても応答できるようにする。
たとえば東京のEdgeに対象のファイルがあれば、東京からOriginのあるリージョンまで取りに行く必要がない。静的アセットだけでなく、設定次第ではキャッシュ可能なHTMLやAPIレスポンスも対象になる。
ここで減るのは主に次のようなコストだ。
- ユーザーがレスポンスを待つ時間
- EdgeからOriginへ向かうリクエスト数
- Originが処理するリクエスト数
- Origin側から外へ転送するデータ量
CDNによっては、Edge POPとOriginの間にRegional Cache、Tiered Cache、Shieldなどの中間層を持つ。EdgeでMISSしても上位キャッシュでHITすれば、Originまで到達しない。
flowchart TD
A[User] --> B[Edge Cache]
B --> C[Regional / Tiered Cache]
C --> D[Origin]ただし、すべてのCDNがこの構成を持つわけではなく、名称や階層もサービスによって違う。ここでは「CDNの中にも複数段のキャッシュがあり得る」という理解で十分だ。
アプリケーションキャッシュはDBや計算処理への到達を減らす
Originまでリクエストが届いても、そのたびにDBへ問い合わせる必要はない。
たとえば商品情報の取得で、更新頻度が低く参照回数が非常に多いなら、Redisなどへ結果を一定時間保持する方法がある。
request
↓
application
↓
Redisにある? ── yes → cached value
↓ no
Database
Redisを使わず、アプリケーションプロセスのメモリへ小さなデータを保持することもある。Reverse ProxyがHTTPレスポンスをキャッシュする構成もある。フレームワークによっては、ルート、レンダリング結果、データ取得結果などに独自のキャッシュ機構を持つ。
この層では、クライアントからAPIへのリクエスト数が減るとは限らない。
クライアントは100回APIを呼んでいても、Redisで100回応答できればDBへの問い合わせは大きく減らせる。何を減らしたいかによって、キャッシュを置く場所が変わる。
DBへ到達しても、ストレージを読んでいるとは限らない
アプリケーションキャッシュを使っていないからといって、DBが毎回SSDからデータを読み込んでいるわけでもない。
MySQLのInnoDB Buffer PoolやPostgreSQLのshared buffersのように、DBは頻繁に使うデータページやインデックスページをメモリ上へ保持する。さらにOSのPage Cacheがストレージへのアクセスを吸収する場合もある。
この層はRedisのようなアプリケーションキャッシュとは性質が違う。Redisで結果を返せばDBへの問い合わせ自体を省けるが、DBのBuffer PoolではSQLの受付や実行計画、可視性の確認などDB側の処理は残る。その代わり、遅いストレージI/Oを避けられる可能性が高くなる。
「DBから取得した」というログだけでは、物理ストレージを読んだかどうかまでは分からない。
同じ「キャッシュ」でも、何を減らしているかが違う
キャッシュを置き場所だけで覚えると、なぜその層が必要なのかが見えにくい。目的をもう一つの軸にすると違いが分かりやすい。
| 目的 | 何を減らす・改善するか | 代表的な例 |
|---|---|---|
| レイテンシ削減 | ユーザーの待ち時間 | Browser Cache、CDN、Memory Cache |
| APIリクエスト削減 | クライアントからAPIへの呼び出し | SWR、TanStack Query、アプリ内データキャッシュ |
| Origin負荷削減 | Originへ届くHTTPリクエスト | CDN、Edge Cache |
| DB負荷削減 | DBへの問い合わせ、重い集計 | Redis、Application Cache |
| ストレージI/O削減 | ディスクやブロックストレージへの読み書き | DB Buffer Pool、OS Page Cache |
| 計算量削減 | 同じ変換や集計の再実行 | Memoization、生成結果のキャッシュ |
| 通信量削減 | 再転送するデータ量 | HTTP Cache、条件付きリクエスト、CDN |
| オフライン対応 | ネットワークへの依存 | Service Worker、Cache Storage、ローカルDB |
| コスト削減 | Compute、DB、転送量など | 上記のキャッシュを目的に応じて組み合わせる |
一つのキャッシュが複数の目的を持つことも多い。CDN Cacheはレスポンスを速くすると同時に、Originへのリクエストと転送量を減らせる。クライアントのデータキャッシュも、表示を速くしながらAPIリクエスト回数を減らせる。
「APIのリクエストを減らす」だけでも3つの意味がある
キャッシュの話で「APIへのアクセスを減らせる」と説明されることがあるが、どこの境界を指しているかで意味が変わる。
クライアントのキャッシュ
Client Cache HIT
↓
APIへリクエストしない
クライアントからAPIへの通信そのものが発生しない。SWRやTanStack Queryなどのデータキャッシュが分かりやすい例だ。
CDNのキャッシュ
Client
↓
CDN Cache HIT
↓
Origin APIへ届かない
クライアントはHTTPリクエストを送っているが、Origin APIは処理しない。ユーザーとEdgeの間では通信しているため、「APIリクエストがゼロ」という意味ではない。
アプリケーションキャッシュ
Client
↓
API
↓
Redis Cache HIT
↓
Databaseへ問い合わせない
APIはリクエストを処理するが、DBアクセスを省ける。
この3つはすべて「キャッシュによってアクセスを減らした」と言える。ただし、減っている場所が違う。性能やコストを考えるときは、どの境界の回数を減らしたいのかまで決める必要がある。
キャッシュを見るときは「どこ」と「何のため」をセットにする
キャッシュの仕組みを調べるときは、製品名を覚えるより次の順番で見ると追いやすい。
- どこにあるか — Client、Edge、Application、Databaseなど
- 何を保持しているか — HTTPレスポンス、APIデータ、計算結果、データページなど
- 何を減らしたいか — 待ち時間、APIリクエスト、Origin負荷、DBアクセス、I/Oなど
- いつ古いと判断するか — TTL、HTTPのfreshness、アプリ独自のstale判定など
- 誰が更新・無効化するか — 自然失効、再検証、Purge、アプリからの削除など
最初の3つが分かると、そのキャッシュが必要な理由が見える。後半の2つは、キャッシュを運用すると必ず問題になる「古い値をいつ捨てるか」の話につながる。
全体像が分かると「変更したのに反映されない」の見方も変わる
キャッシュは、速くするためだけの仕組みではない。同じ値を再利用する以上、更新直後には「変更元は新しいが、途中のキャッシュには古い値が残っている」という状態が発生し得る。
たとえば、次の現象は一見すると別のトラブルに見える。
- CSSやJavaScriptを更新したのに、ブラウザでは以前の内容が表示される
- DBの値を更新したのに、APIから古い値が返る
- CDNをPurgeしたのに、クライアントでは以前のレスポンスが見える
- DNSレコードを変更したのに、一部の環境では以前の接続先が使われる
しかし、どれも「変更した場所」と「利用者が実際に参照している場所」が一致していない可能性がある。
だからキャッシュの問題を調べるときに、「キャッシュを消す」という操作だけでは足りない。まず、どの層がその値を返しているのかを特定する必要がある。
この切り分けは別の記事で詳しく扱うとして、先に持っておきたいのは、ブラウザからDB・ストレージまでの間に複数のキャッシュがあり、それぞれ別の目的で動いているという全体像だ。どこに置かれ、何を減らしているのかが分かれば、キャッシュという言葉だけで異なる仕組みをひとまとめにせずに済む。