SPAの帯域圧迫を解決する3つのキャッシュ戦略【インフラからReact Queryまで】

6b311948f55fb93dda8f9033159e014b
🔧 インフラエンジニア歴12年☁️ AWS実務2.5年📝 Tech Otaku Lab運営

SPAの帯域対策は「インフラ・アプリ・データ」の3層防御で完成します。静的アセットはImmutableキャッシュ、HTMLはno-cache、APIはReact QueryのstaleTime調整の組み合わせが2026年の最適解です。ChunkLoadError対策も忘れずに。

SPAの帯域圧迫を解決する3つのキャッシュ戦略:インフラからReact Queryまで

Webアプリケーション開発において事実上の標準となりつつある Single Page Application (SPA) ですが、初期ロードの重さや帯域圧迫に悩まされていませんか?

本記事では、インフラ層、アプリ層、データ層の3つの視点から、SPAのパフォーマンスを劇的に改善する「多層的防御」アプローチを解説します。数メガバイトのバンドルサイズと戦う全エンジニア必見のキャッシュ戦略ガイドです。

この記事でわかること

  • SPAとMPAの帯域消費モデルの違い
  • HTTPキャッシュの正しい設定方法(Immutable vs No-Cache)
  • Service Workerによるオフライン対応とキャッシュ戦略
  • React Query/SWRによるAPI通信の最適化
  • ChunkLoadErrorを防ぐデプロイ戦略
目次

1. なぜSPAは帯域を圧迫するのか?MPAとの決定的な違い

SPA(Single Page Application)は、単一のHTMLページをロードし、JavaScriptでDOMを動的に書き換えるアーキテクチャです。これによりネイティブアプリのような操作感を実現しますが、その代償として「初期ロード時に大量の資産をダウンロードする必要がある」という課題を抱えています。

SPAとMPAの帯域消費モデル比較

特性 SPA (Single Page Application) MPA (Multi-Page Application)
初期ロード 大 (High)
JSバンドル全体を一括DL
2〜10MB程度
小 (Low)
必要なHTMLのみDL
100〜500KB程度
ページ遷移 極小 (Very Low)
JSONデータのみ取得
数KB〜数十KB
中 (Medium)
HTML全体を再取得
100KB〜1MB
キャッシュ効率 2回目以降は
ほぼゼロ通信
毎回HTML再取得
(一部キャッシュ可)
損益分岐点 3〜5ページ閲覧後
から有利
直帰率が高い
サイト向き
ポイント: 損益分岐点を意識する
SPAは「直帰ユーザー」には非効率ですが、回遊数が増えるほど帯域効率が良くなる「損益分岐点」を持っています。対策の鍵は、いかに初期ロードを減らし、2回目以降の通信をゼロにするかにあります。

よくある失敗例:バンドルサイズの肥大化

実例:あるダッシュボードアプリの悲劇

  • 未使用のライブラリを放置 → バンドルサイズ12MB
  • 画像を最適化せずインポート → 追加で8MB
  • キャッシュ設定なし → 毎回20MB通信
  • 結果: 4G回線で初期ロード45秒、直帰率78%

2. 【インフラ層】HTTPキャッシュの「ハイブリッド戦略」

2.1 静的アセットには「Immutable」戦略

Nginx / CDN Config Example

# JSファイル、CSSファイル、画像などの静的アセット
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|woff2)$ {
    add_header Cache-Control "max-age=31536000, immutable";
    add_header Vary "Accept-Encoding";
}

immutable ディレクティブを付与することで、ブラウザは再読み込み時でもサーバーへの確認(304 Not Modified確認)を行わず、ディスクキャッシュを即座に使用します。これにより、リロード時の通信が完全にゼロになります。

max-age=31536000 の意味: これは「31,536,000秒(= 1年間)キャッシュせよ」という指示です。実質的に「永久キャッシュ」として機能しますが、ファイル名が変われば新しいファイルを取得するため問題ありません。

2.2 エントリーポイントには「No-Cache」戦略

index.html用のヘッダー設定

# HTML(エントリーポイント)
location / {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
    try_files $uri /index.html;
}
注意: no-cache は「キャッシュしない」という意味ではありません。正確には「使う前にサーバーに有効性を確認せよ」という意味です。より厳格にキャッシュを防ぐには no-store を併用します。

2.3 CDNの活用で全世界高速化

🚀 Tech Otaku Lab 推奨CDN構成

  • Cloudflare: 無料プランでも高性能。Brotli圧縮対応
  • AWS CloudFront: S3との連携が容易。invalidation機能が強力
  • 国内CDN: 日本向けなら、さくらCDNやConoHaのCDNオプションも選択肢
サーバー選びのポイント: SPAを運用するなら、静的ファイル配信に強いサーバーが必須です。特にNginxやCDN連携が簡単なサービスを選びましょう。

高速サーバーを確認する(ConoHa WING推奨)
※ 初期費用0円・独自ドメイン無料・かんたん開設

3. 【アプリ層】Service Workerによる制御

主要なキャッシュ戦略の選択肢

推奨 Cache First 戦略

動作: まずキャッシュを確認し、あれば即座に返す。なければネットワークから取得してキャッシュに保存。

最適な対象: 画像、フォント、バージョニングされたJS/CSS

メリット: 帯域削減効果は最大。オフライン動作可能。

デメリット: 更新されたファイルを即座に反映できない(キャッシュクリアが必要)

検討 Stale While Revalidate

動作: キャッシュを即座に表示しつつ、裏で最新データを取得してキャッシュを更新。

最適な対象: API レスポンス、ニュースフィード、タイムライン

メリット: UXは爆速。古いデータでも即座に表示できる。

デメリット: 裏で通信が発生するため帯域削減効果は限定的

慎重に Network First 戦略

動作: まずネットワークから取得を試み、失敗したらキャッシュにフォールバック。

最適な対象: 常に最新が必要なデータ(在庫情報、価格など)

メリット: データの鮮度を最優先できる

デメリット: オンライン時は毎回通信が発生するため帯域節約効果なし

Workboxによる実装例

service-worker.js (Workbox使用)

import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// 画像: Cache First戦略(最大50件、30日間保持)
registerRoute(
  ({request}) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30日
      }),
    ],
  })
);

// API: Stale While Revalidate(最大20件、1日保持)
registerRoute(
  ({url}) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    cacheName: 'api-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 20,
        maxAgeSeconds: 24 * 60 * 60, // 1日
      }),
    ],
  })
);

4. 【データ層】React Query / SWR による重複排除

主要機能と帯域削減効果

機能 説明 帯域削減効果
リクエストの重複排除
(Deduplication)
同一キーへのリクエストを短期間(数ミリ秒)まとめて1回だけ実行 極大
複数コンポーネントが同時にデータ要求しても1回で済む
バックグラウンド再検証
(Background Revalidation)
画面を開いたままでも定期的に最新データを取得
間隔を調整することで制御可能
ウィンドウフォーカス時の再検証 タブを戻った時に自動で最新データを取得
ユーザー体験は向上するが通信は発生
Optimistic Update サーバー応答を待たずにUIを更新(楽観的更新)
体感速度が劇的に向上

React Queryの実装例

useQuery フック(基本)

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    staleTime: 5 * 60 * 1000, // 5分間はフレッシュとみなす
    cacheTime: 10 * 60 * 1000, // 10分間メモリに保持
    refetchOnWindowFocus: false, // タブ切り替え時の再取得を無効化(帯域節約)
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <div>{data.name}</div>;
}
帯域削減のチューニングポイント:

  • staleTime を長く設定(例: 5分〜10分)すると、同じデータへの再リクエストが減る
  • refetchOnWindowFocus: false でタブ切り替え時の自動再取得を無効化
  • refetchInterval を設定しない(ポーリングを避ける)

SWRとの比較

項目 React Query SWR
学習コスト やや高め
(機能が豊富)
低い
(シンプル)
バンドルサイズ 約12KB (gzip) 約5KB (gzip)
デフォルト動作 保守的
(通信控えめ)
積極的
(頻繁に再検証)
帯域削減 細かく制御可能 設定次第
Tech Otaku Lab の結論: 帯域削減を最優先するなら React Query で staleTime を長めに設定する戦略がベスト。UX優先でリアルタイム性が必要なら SWR のデフォルト設定が優秀。

5. 運用上の落とし穴:ChunkLoadError対策

発生メカニズム

1 ユーザーがサイトを開く

古い index.html(v1)をロード。この時点ではJSはまだ読み込まれていない。

2 開発者が新バージョンをデプロイ

サーバー上のファイルが v1 から v2 に置き換わる。古いJSファイル(main.abc123.js)は削除される。

3 ユーザーがページ遷移

v1の index.html が参照する古いJSファイルを要求 → 404 Not Found → ChunkLoadError発生。

対策:アトミックデプロイと旧ファイルの保持

❌ 悪い例:

1. 古いファイルをすべて削除
2. 新しいファイルをアップロード
→ この間にアクセスしたユーザーは全員エラー
✅ 良い例:

1. 新しいファイルを追加アップロード(古いファイルは残す)
2. index.html を最後に上書き
3. 古いファイルは24時間後に自動削除(cron等)
→ 移行期間中も両バージョンが共存

自動リトライの実装

React Router でのエラーハンドリング例

import { useEffect } from 'react';
import { useRouteError } from 'react-router-dom';

export function ErrorBoundary() {
  const error = useRouteError();
  
  useEffect(() => {
    // ChunkLoadError の検出
    if (error?.name === 'ChunkLoadError' || 
        error?.message?.includes('Loading chunk')) {
      
      // 1回だけリロード(無限ループ防止)
      const hasReloaded = sessionStorage.getItem('chunk-error-reload');
      if (!hasReloaded) {
        sessionStorage.setItem('chunk-error-reload', 'true');
        window.location.reload();
      }
    }
  }, [error]);
  
  return <div>アプリケーションエラーが発生しました</div>;
}

6. パフォーマンス計測と改善の指標

指標 理想値 計測方法
初回ロード時間 (FCP) 1.5秒以内 Chrome DevTools / Lighthouse
バンドルサイズ 500KB以下(gzip後) Webpack Bundle Analyzer / vite-plugin-visualizer
キャッシュヒット率 80%以上(2回目以降訪問) CDNダッシュボード / Google Analytics
API呼び出し回数 必要最小限 React Query DevTools / Network Tab
Lighthouseコマンドライン実行

npx lighthouse https://your-spa-site.com \
  --only-categories=performance \
  --output=html \
  --output-path=./lighthouse-report.html

7. 実践ワークフロー:既存SPAへの段階的導入

1 【Week 1】現状計測とボトルネック特定

・Lighthouse でスコア計測
・Webpack Bundle Analyzer で肥大化しているライブラリを特定
・未使用の依存関係を削除

2 【Week 2】インフラ層の最適化

・Nginx / CDN の Cache-Control ヘッダー設定
・Brotli 圧縮の有効化
・画像の WebP 変換と遅延読み込み(Lazy Load)

3 【Week 3】データ層の最適化

・React Query または SWR の導入
・不要な API ポーリングの停止
・staleTime の調整

4 【Week 4】Service Worker の導入(オプション)

・Workbox のセットアップ
・Cache First 戦略の適用
・オフライン対応の実装

8. まとめ:多層防御で完璧なキャッシュ戦略を

Tech Otaku Lab 推奨最終構成

  • 1 静的アセット: Immutableキャッシュ(1年) + Brotli圧縮
  • 2 HTML: no-cache で常に最新を確認
  • 3 Service Worker: Cache First(画像)+ Stale While Revalidate(API)
  • 4 API通信: React Query の staleTime=5分 で無駄撃ちを排除
  • 5 デプロイ: 旧ファイルを24時間保持してChunkLoadError回避

これらを組み合わせ、「必要なものを、必要な時に、一度だけ送る」システムを構築しましょう。

よくある質問(FAQ)

Q1. Cache-Control の immutable は全ブラウザで動作する?

A. Firefox, Chrome, Safari は対応済みですが、古いブラウザ(IE11等)では無視されます。ただし無視されても害はなく、通常の max-age として機能します。

Q2. Service Worker は必須?

A. いいえ。HTTP キャッシュだけでも十分な効果があります。オフライン対応や高度な制御が必要な場合のみ導入を検討してください。

Q3. React Query と SWR、どちらを選ぶべき?

A. 大規模アプリや細かい制御が必要なら React Query。シンプルさ重視なら SWR。どちらも素晴らしいライブラリです。

Q4. CDNを使わなくてもキャッシュ効果はある?

A. はい。ブラウザキャッシュだけでも効果は絶大です。CDNはグローバル配信や初回アクセス高速化のための追加施策です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次