40
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フロントエンドにおけるキャッシュの全レベルを理解する

40
Last updated at Posted at 2026-03-05

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-memocomputed があります。

動作フロー

使い方

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とサーバーの状態がずれる

参考: TanStack Query docs


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-revalidateCache-Control ヘッダーの stale-while-revalidate は同じ原理で動作する — 一方はSWが制御し、もう一方はブラウザが自動的に処理する

参考: web.dev — Service worker caching and HTTP caching


5. Browser HTTP Cache

概念

Browser HTTP Cache はブラウザのビルトインキャッシュで、HTTPレスポンスヘッダーに基づいて動作します。最も自動的に動作するキャッシュレベルです — 開発者はコードを書く必要がなく、サーバーで正しいヘッダーを設定するだけです。

関連するヘッダー:

  • Cache-Control
  • ETag
  • Last-Modified
  • Expires

動作フロー

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-cacheno-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 を深く掘り下げます — 動作の仕組み、重要なヘッダー、バリデーションフロー、実践的なキャッシュ設定について。

40
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?