Webアプリケーションを高速化する技術の中で、キャッシュは最も重要なものの一つです。ただ、キャッシュといっても種類はさまざまです:
- React の memoization
- ブラウザの HTTP cache
- Service Worker の Cache API
- CDN の edge cache
この記事では、それぞれがどこで・どのように動作するのかを整理して解説します。
6つのキャッシュレベルの概要
💡 重要な分類:
- 上位3レベル(Component → Persistent):主にデータをキャッシュ(APIレスポンス、state、オブジェクト)
- 下位3レベル(Service Worker → CDN):主にリソースをキャッシュ(JS、CSS、画像、HTML)
下に行くほど:キャッシュのスコープが広くなり、データの保持期間が長くなり、フロントエンド開発者のコントロールが減っていきます。
1. Component Cache(UIキャッシュ)
概念
Component cache は、データが変わっていないときにUIの再レンダリングを避ける技術です。これは React、Vue、Solid などの UI framework/library に存在する概念であり、ブラウザのビルトイン機能ではありません。
これらのフレームワークでは、コンポーネントが複雑だったり、ツリーが大きかったり、何度もレンダリングされる場合、再レンダリングのコストが高くなることがあります。Component cache は以前のレンダリング結果を再利用します。
この記事では React を例として使いますが、Vue には同様の仕組みとして v-memo と computed があります。
動作フロー
使い方
React.memo — propsが変わらなければレンダリングをスキップ:
const ExpensiveComponent = React.memo(function ({ data }) {
console.log("render");
return <div>{data}</div>;
});
useMemo — 計算結果をキャッシュ:
const value = useMemo(() => expensiveCalc(data), [data]);
useCallback — 関数の参照をキャッシュ:
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
いつ使うべきか
実際に計測されたパフォーマンス問題がある場合にのみ使用する:
- 不必要な再レンダリングが多発している(React DevTools Profilerで確認できる)
- 毎レンダリングで重い計算が走っている
- 複雑なアイテムが多い大規模なリスト
注意
React.memoは毎レンダリングでpropsを比較するオーバーヘッドがある — コンポーネントがシンプルだったり、propsが頻繁に変わる場合は、memoを使わない方が速いことがあるuseMemo/useCallbackの dependency array が間違っていると → キャッシュが古い値を返し続け、バグを見つけにくい
2. Runtime Memory Cache(データキャッシュ)
概念
Runtime memory cache はアプリケーションのRAMにデータをキャッシュします — 例:APIレスポンス、computed data、global store。このキャッシュはタブのライフサイクルの中にのみ存在します。
動作フロー
基本的な例
const cache = new Map();
async function fetchUser(id) {
if (cache.has(id)) {
return cache.get(id);
}
const data = await fetch(`/api/user/${id}`).then(r => r.json());
cache.set(id, data);
return data;
}
React エコシステムでの利用
React Query(TanStack Query):
// React Query v4
useQuery(["user", id], fetchUser, {
staleTime: 1000 * 60, // 1分間データを"fresh"とみなす
cacheTime: 1000 * 60 * 5, // アンマウント後5分間キャッシュを保持
})
// React Query v5 — "cacheTime" は "gcTime" にリネームされた
useQuery({
queryKey: ["user", id],
queryFn: fetchUser,
staleTime: 1000 * 60,
gcTime: 1000 * 60 * 5,
})
SWR:
useSWR("/api/user", fetcher)
キャッシュの無効化
// ミューテーション成功後
queryClient.invalidateQueries(["user", id])
// または直接データをセット
queryClient.setQueryData(["user", id], newData)
注意
- 頻繁に変わるデータで
staleTimeを高く設定しすぎると → コンポーネントが再マウントされても React Query がリフェッチしないため、キャッシュが切れるまでユーザーは古いデータを見続ける- TTLがない、または無効化されないキャッシュ → メモリが徐々に増加し、長時間動作するSPAでは特に危険
- ミューテーション後はキャッシュを手動で無効化または更新する必要がある — 怠ると、UIとサーバーの状態がずれる
3. Persistent Cache(Storage / IndexedDB)
概念
ブラウザにデータを長期保存するキャッシュです — リフレッシュ、タブを閉じる、ブラウザの再起動後も保持されます。
| ストレージ | 容量 | 同期 | 使いどころ |
|---|---|---|---|
| localStorage | ~5MB | 同期 | 小さくシンプルなデータ |
| sessionStorage | ~5MB | 同期 | セッション中だけ必要なデータ |
| IndexedDB | はるかに大きい | 非同期 | 大きなデータ、複雑な構造 |
動作フロー
localStorage
// キャッシュを保存
localStorage.setItem("user", JSON.stringify(data));
// キャッシュを読み込む
const user = JSON.parse(localStorage.getItem("user"));
IndexedDB
データが大きい、または複雑なクエリが必要な場合(オフラインデータ、大きなAPIキャッシュ):
import { openDB } from 'idb';
const db = await openDB('app-cache', 1, {
upgrade(db) {
db.createObjectStore('data');
}
});
Cache then Network
async function getData(key) {
// 1. まずキャッシュから取得 → 即座に返す
const cached = await getFromIndexedDB(key);
// 2. バックグラウンドでネットワークからフェッチしてキャッシュを更新
fetchFromNetwork(key).then(async (freshData) => {
await saveToIndexedDB(key, freshData);
notifyUIUpdate();
});
return cached;
}
注意
- デプロイ後にデータ構造が変わってもマイグレーションがない → アプリがlocalStorageから古いデータを読んでクラッシュまたは誤表示する
- センシティブなデータ(トークン、個人情報)をlocalStorageに保存しない — これはプレーンテキストであり、あらゆるスクリプトから読み取られる可能性がある
4. Service Worker Cache(プロキシレイヤー)
概念
Service Worker はアプリケーションとネットワークの間のプロキシレイヤーとして動作します。ネットワークリクエストをインターセプトし、SWキャッシュから返すか、ネットワークに転送するか、その組み合わせかを決定します。
リクエストフロー内での位置
Cache patterns
Cache First — キャッシュ優先、ネットワークにフォールバック:
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
});
Network First — ネットワーク優先、キャッシュにフォールバック:
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
Stale While Revalidate — キャッシュを即座に返し、バックグラウンドで更新:
event.respondWith(
caches.open('my-cache').then(async (cache) => {
const cached = await cache.match(event.request);
const fetchPromise = fetch(event.request).then(networkRes => {
cache.put(event.request, networkRes.clone());
return networkRes;
});
return cached || fetchPromise;
})
);
デプロイ時のキャッシュ無効化
const CACHE_NAME = 'my-app-v2';
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
})
);
});
注意
- リアルタイムデータ(チャット、ライブ価格、通知)が必要なアプリでは Cache First を使うべきではない — SWは新しいデータをフェッチする代わりに古いデータを返してしまう
- 適切なアップデートフローなしに新バージョンをデプロイすると → すべてのタブを閉じるまでユーザーは古いSWで動き続け、新しいコードを受け取れない
- SWのfetchハンドラー内のエラーはアプリ全体が読み込めなくなる可能性がある — try/catchで丁寧にラップする必要がある
- SWの
stale-while-revalidateとCache-Controlヘッダーのstale-while-revalidateは同じ原理で動作する — 一方はSWが制御し、もう一方はブラウザが自動的に処理する
参考: web.dev — Service worker caching and HTTP caching
5. Browser HTTP Cache
概念
Browser HTTP Cache はブラウザのビルトインキャッシュで、HTTPレスポンスヘッダーに基づいて動作します。最も自動的に動作するキャッシュレベルです — 開発者はコードを書く必要がなく、サーバーで正しいヘッダーを設定するだけです。
関連するヘッダー:
Cache-ControlETagLast-ModifiedExpires
動作フロー
Cache-Control
Cache-Control: public, max-age=3600
意味:リソースは1時間有効。
条件付きリクエスト
キャッシュがstaleになると、ブラウザは全体を再フェッチするのではなく、条件付きリクエストを送ってサーバーに「リソースは変わりましたか?」と問い合わせます:
- サーバーが
ETagを返したことがある → ブラウザはIf-None-Match: <etag-value>を付けて送る - サーバーが
Last-Modifiedを返したことがある → ブラウザはIf-Modified-Since: <date>を付けて送る - サーバーが両方を返した場合は
If-None-Matchが優先される
サーバーのレスポンス:
- 304 Not Modified(ボディなし)→ ブラウザはキャッシュを再利用、帯域幅を節約
- 200 OK(新しいボディ付き)→ ブラウザがキャッシュを更新
no-cache vs no-store
最も混乱しやすいポイント — 名前から誤解しやすい:
| ディレクティブ | 実際の意味 |
|---|---|
no-cache |
キャッシュは保存するが、使用前にサーバーでバリデーションが必要 |
no-store |
何も保存しない — どこにもキャッシュしない、センシティブなデータに使用 |
⚠️
no-cacheは 「キャッシュしない」という意味ではない — これは非常によくある誤解です。
ベストプラクティス
1. 静的アセットのフィンガープリント:
main.abc123.js ← ビルド1回目
main.xyz456.js ← ビルド2回目、ファイル名が変わる
max-age=31536000, immutable と組み合わせる — 1年間キャッシュ、再バリデーションなし。
2. APIレスポンスのキャッシュ(ユーザー固有):
Cache-Control: private, max-age=60
private によりCDNやプロキシがユーザーのデータを保存しないことを保証。
3. バリデーション付きHTMLキャッシュ:
Cache-Control: no-cache
ETag: "33a64df5"
no-cache はここでは:ブラウザはキャッシュを保存するが、使用前に必ずサーバーでバリデーションする、という意味。サーバーが304を返した場合、ブラウザはHTMLを再ダウンロードする帯域幅を使わずにキャッシュを再利用。
デプロイ時のキャッシュ無効化
| リソースの種類 | 対処法 |
|---|---|
| CSS / JS | ファイル名を変更(webpackハッシュ) |
| API | URLにバージョンを含める(/api/v2/users) |
| クエリパラメータ | キャッシュバスティング(?v=2) |
注意
- フィンガープリントのないファイルに長い
max-ageを設定する → デプロイ後もキャッシュが切れるまでユーザーは古いファイルを使い続ける- ユーザー固有のデータを返すAPIに
Cache-Control: publicを使う → 共有キャッシュ(プロキシ、CDN)がユーザーAのデータをユーザーBに返す可能性があるno-cacheとno-storeを混同する → 不必要なリクエストを毎回バリデートするか、本来キャッシュすべきときにキャッシュしないかのどちらかになる
参考:
6. CDN Edge Cache
概念
CDNはサーバーオリジンよりもユーザーの近くにキャッシュを配置します。世界中に広がる**PoP(Point of Presence)**ネットワークを通じて。主要なCDN:Cloudflare、Fastly、Akamai、AWS CloudFront。
動作フロー
重要なヘッダー
Cache-Control: public, max-age=86400, s-maxage=86400
s-maxage はCDN/プロキシキャッシュ専用 — ブラウザとは異なるTTLを設定できます。
キャッシュの無効化
API経由でのパージ(Cloudflareの例):
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/main.js"]}'
URLのバージョニング:
https://cdn.example.com/v2/main.js
Cache-Tag(Cloudflare):
Cache-Tag: blog-post-42
ファイルごとではなくタグでパージ — 一つの変更が多くのリソースに影響する場合に非常に便利。
リソースの種類別キャッシュ設定
| 種類 | ヘッダー | 説明 |
|---|---|---|
| 静的アセット(JS/CSS) | public, max-age=31536000, immutable |
1年間キャッシュ、変更なし |
| HTMLページ | public, max-age=60, s-maxage=60 |
ブラウザとCDNの両方で1分間キャッシュ |
| APIレスポンス | public, max-age=0, s-maxage=300 |
ブラウザではキャッシュしない、CDNで5分間キャッシュ |
注意
- 動的データを返すAPIに高い
s-maxageを設定する → 全ユーザーが同じ古いレスポンスをCDNから受け取り、ユーザーを区別できなくなる- 新バージョンのデプロイ時にCDNのパージを忘れる → ユーザーは新しいHTML/JSを受け取るが、アセットは古いバージョンのまま、アプリがクラッシュする可能性がある
Cache-TagとパージAPIはCI/CDパイプラインに統合する必要がある — 手動でやると見落としが非常に起きやすい
まとめ:実際のリクエストフロー
ユーザーが初めてダッシュボードページを開いた場合:
各レベルはリクエストがより遠くへ行く前に「止める」機会です。ユーザーに近いレベルほど、レスポンスが速くなります。
まとめ
フロントエンドにおいて、キャッシュは一箇所にしか存在しません。一つのリクエストは、それぞれ異なる目的を持つ複数のレベルを経由することがあります:
| レベル | 種類 | ライフタイム | 主な目的 |
|---|---|---|---|
| Component | UI | レンダリング中 | 不必要な再レンダリングを防ぐ |
| Runtime Memory | データ | タブセッション中 | APIの重複呼び出しを防ぐ、UIを高速化 |
| Persistent Storage | データ | 永続的(削除まで) | オフライン、再オープン時の高速読み込み |
| Service Worker | ハイブリッド | 設定可能 | オフライン、詳細なリクエスト制御 |
| HTTP Cache | リソース |
max-age による |
ネットワーク負荷を減らす |
| CDN Edge | リソース |
s-maxage による |
レイテンシを減らす、オリジンの負荷を減らす |
これらのレベルを理解することで:
- データの種類に応じた適切なキャッシュ方法を選定できる
- 不必要なAPIリクエストの数を減らせる
- アプリケーション全体のパフォーマンスを向上させられる
- デプロイ時に正しくキャッシュを無効化できる
次の記事では HTTP Cache を深く掘り下げます — 動作の仕組み、重要なヘッダー、バリデーションフロー、実践的なキャッシュ設定について。