1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フロントエンドのキャッシュ戦略を整理する:HTTP・React Query・Service Worker・IndexedDB

1
Last updated at Posted at 2026-04-27

Webサイトを構築するとき、ユーザーが画面を開き直すたびにサーバーへリクエストが飛ぶのはパフォーマンス上の問題になりやすい。本記事ではブラウザ側で利用できるキャッシュ・ストレージの仕組みを、全体像から順に整理する。


1. 全体像:キャッシュの種類と目的

ブラウザで使えるキャッシュ・ストレージは大きく2つの目的に分類できる。

通信の効率化(キャッシュ)
├── HTTPキャッシュ        → サーバーのヘッダー設定だけ、一番シンプル
├── React Query / SWR    → UIと自動連携、SPAに最適
└── Service Worker       → オフライン対応、最も強力

データの永続化(ストレージ)
├── LocalStorage         → 設定・UI状態(5MB以内)
└── IndexedDB            → 大量の構造化データ、オフライン検索

それぞれ保存場所・有効期間・用途が異なる。

HTTPキャッシュ React Query Service Worker LocalStorage IndexedDB
保存場所 ディスク RAM ディスク ディスク ディスク
ページを閉じても残る
オフラインで動く
UIと自動連携
容量 ブラウザ管理 数十MB 数十〜数百MB 〜5MB 数百MB〜GB
インストール 不要 不要 不要 不要 不要

2. ブラウザのメモリ構造を理解する

キャッシュの話をする前に、ブラウザがどのようにメモリを使っているかを把握しておく。

モダンブラウザはタブごとに独立したプロセスを起動する。1つのページを開いただけで、以下のようにメモリが消費される。

順位 項目 目安 補足
1 画像・動画のデコードデータ 数十〜数百MB 圧縮前のフルサイズがRAMに展開される
2 GPUテクスチャ/コンポジットレイヤー 数十〜100MB以上 CSSアニメーション、will-changeなどで生成
3 JavaScriptヒープ(GC管理) 数MB〜100MB SPAやフレームワークほど増加
4 DOMツリー 数MB〜数十MB ノード数が多いほど増加
5 CSSスタイル計算結果 数MB〜十数MB 全ノードへの適用済みスタイル

実際のメモリ使用状況はブラウザのDevToolsで確認できる。F12 → Memory タブ → Take snapshot を実行すると、現時点のJavaScriptヒープの内訳がオブジェクトごとに表示される。どのオブジェクトがどれだけメモリを占有しているかを特定するのに役立つ。

特に画像は要注意で、2000×1500のJPEGはファイルサイズ500KBでも、デコード後は以下のサイズになる。

2000 × 1500 × 4バイト(RGBA)= 約12MB

ページに20枚あれば240MBになる。

またRAM上のキャッシュとディスク上のキャッシュでは性質が大きく異なる。

種類 保存場所 特徴
メモリキャッシュ RAM 高速だがタブを閉じると消える
ディスクキャッシュ SSD / HDD ブラウザを閉じても残る

3. HTTPキャッシュ

最もシンプルなキャッシュ。サーバーのレスポンスヘッダーを設定するだけで動く。

保存場所

ブラウザのディスク上に保存される。

Chrome  (Windows): C:\Users\{user}\AppData\Local\Google\Chrome\User Data\Default\Cache
Chrome  (Mac):     ~/Library/Caches/Google/Chrome/Default/Cache
Firefox (Windows): C:\Users\{user}\AppData\Local\Mozilla\Firefox\Profiles\{profile}\cache2
Firefox (Mac):     ~/Library/Caches/Firefox/Profiles/{profile}/cache2

取得ロジック

リクエスト発生
  ↓
メモリキャッシュにあるか? → YES → 即返す(RAM)
  ↓ NO
ディスクキャッシュにあるか? → NO → サーバーへリクエスト
  ↓ YES
有効期限内(max-age未超過)か? → YES → そのまま返す(通信ゼロ)
  ↓ NO
ETag / Last-Modified があるか? → NO → フルリクエスト
  ↓ YES
条件付きリクエスト送信(If-None-Match / If-Modified-Since)
  ├─ 304 Not Modified → キャッシュを再利用(ボディなし、軽い)
  └─ 200 OK          → 新データを保存して返す

主要なヘッダー

Cache-Control: max-age=300, stale-while-revalidate=60
  • max-age=300:300秒間はサーバーに問い合わせすらしない
  • stale-while-revalidate=60:期限切れでも古いデータを即返しつつ、バックグラウンドで更新

304レスポンスはボディなしなので、通信量を大幅に削減できる。

向いている用途

画像・フォント・CSS・JSなど、変更頻度が低い静的ファイル全般。


4. React Query / SWR

HTTPキャッシュはUIと連携しない。React QueryやSWRはJavaScript側でキャッシュを管理し、データが更新されたら自動で再レンダリングしてくれる。

HTTPキャッシュとの違い

HTTPキャッシュ React Query / SWR
管理場所 ブラウザ(ディスク・RAM) JavaScriptのメモリ(ヒープ)
管理者 ブラウザ自身 アプリのコード
単位 URL クエリキー(自由に定義)
UIとの連携 なし 自動で再レンダリング

保存場所

JavaScriptヒープ上のグローバルなMapオブジェクト。タブを閉じると消える。

// 内部イメージ
const cache = new Map([
  ['articles',      { data: [...], updatedAt: 1714180000 }],
  ['articles', '1', { data: {...}, updatedAt: 1714180001 }],
])

取得ロジック

useQuery() が呼ばれる
  ↓
キャッシュ(Map)にこのキーがあるか?
  ├─ NO  → loading表示 → APIフェッチ → UIを再レンダリング
  └─ YES → キャッシュを即座に表示
              ↓
           staleTime を超えたか?
             ├─ NO(新鮮) → そのまま表示継続
             └─ YES → バックグラウンドでリフェッチ
                          ↓
                       成功 → キャッシュ更新 → UIを再レンダリング
                       失敗 → リトライ or エラー表示
                          ↓
                       gcTime 経過後 → メモリから解放(GC)

主要な設定

useQuery({
  queryKey: ['articles'],
  queryFn: () => fetch('/api/articles').then(r => r.json()),
  staleTime: 5 * 60 * 1000,  // 5分間はフレッシュ(再フェッチしない)
  gcTime:   10 * 60 * 1000,  // 10分後にメモリから解放
})
  • staleTime:キャッシュを「新鮮」とみなす時間。CMSの記事データなら5〜10分が現実的。デフォルトは0(毎回バックグラウンドリフェッチが走る)
  • gcTimestaleTimeより長く設定するのが基本

向いている用途

SPAのAPIデータ取得全般。Headless CMSの記事一覧・記事内容など。


5. Service Worker

ブラウザとサーバーの間に入る独立したスレッド。全リクエストを横取りしてキャッシュを返せるため、オフライン対応まで可能な最も強力な仕組み。

他との違い

HTTPキャッシュ React Query Service Worker
動く場所 ブラウザ内部 JSヒープ 独立したスレッド
保存場所 ディスク RAM Cache Storage(ディスク)
ページを閉じても残る
オフラインで動く
JSコードで制御

保存場所

HTTPキャッシュとは完全に別の専用領域「Cache Storage」に保存される。ブラウザのDevToolsの Application → Cache Storage で確認できる。

ライフサイクル

① install   → 静的ファイルをCache Storageに保存(一度だけ)
② activate  → 古いキャッシュを削除・引き継ぎ(一度だけ)
③ fetch     → 全リクエストを横取りしてキャッシュorサーバーへ(毎回)

3つのキャッシュ戦略

用途に応じて戦略を使い分ける。

① Cache First(キャッシュ優先)

キャッシュがあれば即返す。なければサーバーへ。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request).then(response => {
        caches.open('v1').then(c => c.put(event.request, response.clone()));
        return response;
      });
    })
  );
});

用途:画像・フォント・CSSなど変更の少ないファイル

② Network First(ネットワーク優先)

常にサーバーへ。オフライン時だけキャッシュにフォールバック。

self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        caches.open('v1').then(c => c.put(event.request, response.clone()));
        return response;
      })
      .catch(() => caches.match(event.request))
  );
});

用途:APIレスポンスなど常に最新が必要なデータ

③ Stale While Revalidate(両立)

キャッシュを即返しつつ、バックグラウンドで更新。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('v1').then(cache => {
      return cache.match(event.request).then(cached => {
        const fetchPromise = fetch(event.request).then(response => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetchPromise;
      });
    })
  );
});

用途:CMSの記事など多少古くてもOKだが更新もしたいデータ


6. LocalStorage と IndexedDB

これらはキャッシュというよりもアプリの状態・データを永続化するためのストレージ。どちらもインストール不要でブラウザ標準搭載。

LocalStorage

シンプルなキーバリュー形式。容量は5MB程度。

localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // → 'dark'

有効期限の仕組みがないため、APIデータのキャッシュには向かない。UIの設定・言語設定・既読管理など小さなデータに使う。

IndexedDB

ブラウザ標準搭載の本格的なデータベース。数百MB〜GB級のデータをインデックス検索できる。

LocalStorage IndexedDB
容量 〜5MB 数百MB〜GB級
データ形式 文字列のみ JSオブジェクトそのまま
検索 なし インデックス検索可
非同期 同期(UIブロック) 非同期

保存場所はブラウザのディスク上。

Chrome  (Windows): C:\Users\{user}\AppData\Local\Google\Chrome\User Data\Default\IndexedDB\
Chrome  (Mac):     ~/Library/Application Support/Google/Chrome/Default/IndexedDB/
Firefox (Windows): C:\Users\{user}\AppData\Roaming\Mozilla\Firefox\Profiles\{profile}\storage\
Firefox (Mac):     ~/Library/Application Support/Firefox/Profiles/{profile}/storage/

素のAPIはやや冗長なので、ラッパーライブラリのDexie.jsを使うのが現実的。

import Dexie from 'dexie';

const db = new Dexie('my-app-db');
db.version(1).stores({
  articles: '++id, title, date',
});

// 書き込み
await db.articles.put({ title: '記事A', body: '...', date: new Date() });

// インデックス検索
const recent = await db.articles.where('date').above(someDate).toArray();

7. ユーザーへの許可ダイアログは必要か

機能 ダイアログ
LocalStorage 不要
IndexedDB 不要
Cookie 不要
Service Worker 不要
位置情報・通知・カメラ 必要

CookieバナーはCookieを使うから出すのではなく、個人情報を追跡するから出す。
GDPRや個人情報保護法などの法律上の要件であり、技術的な制約ではない。記事キャッシュ用途であれば同意バナーは不要。

ただし以下の点は注意が必要。

  • 容量超過時にブラウザが古いデータを自動削除することがある
  • プライベートブラウジングではセッション終了時にすべて消える
  • ユーザーが「サイトのデータを消去」するとすべて消える

8. Headless CMS × Reactでのおすすめ構成

リソース おすすめ戦略 理由
画像・フォント・CSS Service Worker(Cache First) ほぼ変わらない
記事一覧・記事内容 React Query + Service Worker(Stale While Revalidate) 多少古くてもOK、更新もしたい
ユーザー固有データ Service Worker(Network First) 常に最新が必要
UI設定・言語 LocalStorage 小さなデータ、永続化が必要
大量記事のオフライン対応 IndexedDB + Dexie.js 構造化・検索が必要

導入ステップ

一度にすべてを実装する必要はない。以下の順番で段階的に導入するのが現実的。

Step 1: HTTPキャッシュ(Cache-Controlヘッダーの設定)
  → コード変更ほぼなし、即効性が高い

Step 2: React Query の導入
  → UIとデータ取得の分離、キャッシュの自動管理

Step 3: Service Worker の追加(Workbox を使うと実装が楽)
  → オフライン対応、より強力なキャッシュ制御

Step 4: IndexedDB の追加(必要な場合のみ)
  → 大量データのオフライン検索が必要になったとき

HTTPキャッシュとReact Queryの併用がまず現実的な第一歩。オフライン対応が必要になったタイミングでService WorkerとIndexedDBを追加していく形がおすすめ。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?