SSRとCSRのレンダリングモード
CharlieはMegaShopのすべてのページがSSRを使用していることを発見し, サーバーに大きな負荷がかかっていました。数百万の商品ページがリクエストごとにレンダリングされています。ホームページのコンテンツはめったに変わらないのに, リクエストのたびに再レンダリングされます。管理画面はSEO不要なのにサーバーでレンダリングされています。ハイブリッドレンダリングにより, 各ページに最適な戦略を選べます:ホームページはSSG, 商品一覧はISR, 詳細ページはSSR, ユーザーセンターはCSR。
1. 学ぶ内容
- routeRulesレンダリングモード設定:ssr/csr/swr/prerender/hydrate
- ISR (インクリメンタル静的再生成):SWRキャッシュとオンデマンド再検証
- ESRエッジレンダリング:Nitro CDNエッジコンピューティング
- ハイブリッドレンダリングの実践ガイド:異なるページに異なる戦略を選択
- MegaShop:数百万商品ページのISRキャッシング設計
2. アーキテクトのある本当の話
(1) 課題:全ページSSRでサーバーが過負荷
MegaShopは毎秒5,000リクエストを処理し, すべてSSRです。ホームページのコンテンツは1日1回しか変わらないのに, リクエストごとにレンダリングされます。商品一覧は1時間ごとに更新され, 100万商品の詳細ページが繰り返しレンダリングされています。サーバーのCPU使用率は90%に達し, Bobはサーバーを10台追加しようとしています。
(2) ハイブリッドレンダリングの解決策
Nuxt 3のrouteRulesにより, 各ルートに独自のレンダリング戦略を設定できます:
TYPESCRIPT
// nuxt.config.ts
routeRules: {
'/': { prerender: true }, // ホーム:ビルド時に1回
'/products': { swr: 3600 }, // 商品一覧:1時間キャッシュ
'/products/**': { swr: 86400 }, // 商品詳細:1日キャッシュ
'/admin/**': { ssr: false } // 管理画面:クライアントのみ
}
(3) メリット:サーバー負荷85%削減
ハイブリッドレンダリングの導入後, SSRリクエストは毎秒5,000から750に減少し, サーバー台数は10台から3台に削減され, コストが70%カットされました。
3. routeRulesレンダリングモード
(1) ハイブリッドレンダリング判定ツリー
flowchart TB
A[ページリクエスト] --> B{SEOが必要?}
B -->|いいえ| C[CSR - ssr: false]
B -->|はい| D{コンテンツは変化する?}
D -->|しない| E[SSG - prerender: true]
D -->|めったに| F[ISR - swr: 秒数]
D -->|頻繁に| G[SSR - デフォルトモード]
D -->|地域による| H[ESR - エッジレンダリング]
C --> I[管理画面 / ダッシュボード / ユーザーセンター]
E --> J[About / 法的情報 / ランディング]
F --> K[商品一覧 / カテゴリページ]
G --> L[商品詳細 / 検索結果]
H --> M[グローバルホーム / 地域コンテンツ]
(2) routeRulesオプションクイックリファレンス
| オプション | 値 | 効果 | 適用シナリオ |
|---|---|---|---|
| ssr | boolean | SSRスイッチ | false = CSR |
| csr | boolean | CSRスイッチ | 従来オプション |
| swr | number/string | ISRキャッシュ期間 (秒) | 定期更新ページ |
| prerender | boolean | ビルド時に事前レンダリング | 静的ページ |
| hydrate | boolean | ハイドレーションのみ, レンダリングなし | 特殊最適化 |
| redirect | string | 301/302リダイレクト | URL移行 |
| cors | boolean | クロスオリジン | APIルート |
| headers | object | カスタムレスポンスヘッダー | キャッシュ/セキュリティ |
(1) ▶ サンプル:完全なrouteRules設定
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// ホーム:ビルド時に事前レンダリング
'/': { prerender: true },
// 静的ページ
'/about': { prerender: true },
'/privacy': { prerender: true },
// 商品一覧:ISR 1時間キャッシュ
'/products': { swr: 3600 },
// 商品詳細:ISR 24時間キャッシュ
'/products/**': { swr: 86400 },
// カテゴリ:ISR 6時間キャッシュ
'/categories/**': { swr: 21600 },
// 管理画面:クライアント側レンダリングのみ
'/admin/**': { ssr: false },
// ユーザーページ:クライアント側
'/profile/**': { ssr: false },
// API:CORSヘッダー
'/api/**': { cors: true },
// リダイレクト
'/shop/': { redirect: '/products/', redirectCode: 301 },
// 静的アセットのカスタムヘッダー
'/_nuxt/**': { headers: { 'cache-control': 'max-age=31536000' } }
}
})
出力:
TEXT
// 実行成功
4. ISRインクリメンタル静的再生成
(1) ISRのワークフロー
sequenceDiagram
participant U as ユーザー
participant C as CDN/Cache
participant S as Nuxt Server
participant D as データベース
U->>C: GET /products/123
C->>C: キャッシュHIT? (swr期間内)
C-->>U: キャッシュ済みHTML (即座)
Note over C,S: swr期間経過後
U->>C: GET /products/123
C->>C: キャッシュSTALE
C-->>U: 古いHTML (高速)
C->>S: バックグラウンド再検証
S->>D: 最新データを取得
D-->>S: 更新データ
S-->>C: 新鮮なHTML
Note over C: 次回リクエストで新鮮なHTMLを取得
(2) ISR, SSR, SSGの比較
| 項目 | SSR | SSG (prerender) | ISR (swr) |
|---|---|---|---|
| レンダリングタイミング | 各リクエスト時 | ビルド時 | キャッシュ期限後のバックグラウンド更新 |
| レスポンスタイム | 🟡 中 | 🟢 最速 | 🟢 高速 (キャッシュHIT時) |
| データ鮮度 | ✅ 最新 | ❌ ビルド時に固定 | ⚠️ 最大遅延 = swr秒数 |
| サーバー負荷 | 🔴 高 | 🟢 最低 | 🟢 低 |
| ユースケース | リアルタイムデータ | 変更なし | 定期更新 |
(1) ▶ サンプル:ISR商品一覧ページ
TYPESCRIPT
// nuxt.config.ts - ISR設定
export default defineNuxtConfig({
routeRules: {
// 商品一覧:キャッシュから配信, 60秒ごとに再検証
'/products': { swr: 60 },
// 商品詳細:1時間キャッシュ
'/products/**': { swr: 3600 }
}
})
出力:
TEXT
// 実行成功
(2) ▶ サンプル:プログラムによるISRキャッシュ無効化
TYPESCRIPT
// server/api/invalidate-cache.post.ts
export default defineEventHandler(async (event) => {
const { path } = await readBody(event)
// 特定キャッシュルートをパージ
await useStorage('cache').removeItem(`nitro:routes:${path}`)
return { message: `Cache invalidated for ${path}`, timestamp: Date.now() }
})
出力:
TEXT
// 実行成功
5. ESRエッジレンダリング
(1) エッジレンダリングの原理
ESR (Edge Side Rendering)はCDNエッジノードでレンダリングを行い, ユーザーは最も近いノードからHTMLを取得します。
| 項目 | 中央SSR | ESR |
|---|---|---|
| サーバーの場所 | 単一リージョン | グローバルエッジノード |
| レイテンシ | 距離に依存 | 最小 (ローカルアクセス) |
| ユースケース | 単一リージョンユーザー | グローバルユーザー |
| Nitroプリセット | node-server | cloudflare-pages / vercel-edge |
(1) ▶ サンプル:Vercel Edgeデプロイ設定
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel-edge'
},
routeRules: {
// ホームはグローバルユーザー向けにエッジでレンダリング
'/': { swr: 3600 },
'/products/**': { swr: 86400 }
}
})
出力:
TEXT
// 実行成功
6. MegaShopハイブリッドレンダリング戦略
(1) MegaShopページレンダリング戦略の計画
| ページ | URL | 戦略 | SWR | 理由 |
|---|---|---|---|---|
| ホーム | / | SSG | - | 安定コンテンツ, ビルド時レンダリング |
| About | /about | SSG | - | ほぼ変更なし |
| 商品一覧 | /products | ISR | 3600 | 1時間ごとに更新 |
| カテゴリ | /categories/** | ISR | 21600 | 毎日更新 |
| 商品詳細 | /products/** | ISR | 86400 | 毎日更新 |
| 検索結果 | /search | SSR | - | リアルタイムクエリ |
| カート | /cart | CSR | - | 会員のみ |
| ユーザーセンター | /profile/** | CSR | - | SEO不要 |
| 管理画面 | /admin/** | CSR | - | SEO不要 |
| API | /api/** | - | - | データ専用API |
(1) ▶ サンプル:MegaShopの完全なrouteRules
TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
routeRules: {
// SSG:ビルド時に事前レンダリング
'/': { prerender: true },
'/about': { prerender: true },
'/privacy': { prerender: true },
'/contact': { prerender: true },
// ISR:再検証付きキャッシュ
'/products': { swr: 3600 }, // 1時間
'/categories/**': { swr: 21600 }, // 6時間
'/products/**': { swr: 86400 }, // 24時間
// SSR:リアルタイムレンダリング
'/search': { swr: 0 },
// CSR:クライアント側のみ
'/cart': { ssr: false },
'/checkout/**': { ssr: false },
'/profile/**': { ssr: false },
'/admin/**': { ssr: false },
// API
'/api/**': { cors: true },
// 静的アセット:長期キャッシュ
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
'/images/**': { headers: { 'cache-control': 'public, max-age=86400' } }
}
})
出力:
TEXT
// 実行成功
(2) 数百万商品ページのISRキャッシュ期限切れ対策
| イベント | 期限切れポリシー | 実装方法 |
|---|---|---|
| 商品価格更新 | 対象商品ページを無効化 | useStorage('cache').removeItem() |
| 新商品追加 | 商品一覧を期限切れ | /productsキャッシュを一括無効化 |
| カテゴリ変更 | カテゴリページを無効化 | /categories/xxxを無効化 |
| サイト全体セール | 全商品ページをクリア | キャッシュ全体をクリア |
| 在庫変動 | 対象商品を期限切れ | Webhookでトリガー |
7. 総合例:MegaShopレンダリングモードの検証
VUE
<!-- pages/admin/dashboard.vue - CSRモード -->
<template>
<div>
<h1>Admin Dashboard</h1>
<p>Rendering mode: Client-Side Only</p>
<p>Current time: {{ currentTime }}</p>
</div>
</template>
<script setup lang="ts">
// CSR:このページはサーバーレンダリングされない
definePageMeta({ ssr: false, layout: 'sidebar' })
const currentTime = ref(new Date().toISOString())
onMounted(() => {
setInterval(() => {
currentTime.value = new Date().toISOString()
}, 1000)
})
</script>
VUE
<!-- pages/products/[id].vue - ISRモード -->
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
<p>${{ product.price }} USD</p>
<p>Cache: revalidated every 24 hours</p>
</div>
</template>
<script setup lang="ts">
// ISR:routeRules swr: 86400で24時間キャッシュ
const route = useRoute()
const { data: product } = await useFetch(`/api/products/${route.params.id}`)
</script>
❓ よくある質問
Q swrの単位は何ですか?
A 秒です。
swr: 60は60秒のキャッシュを示します。期限後, 次のリクエストでバックグラウンド更新がトリガーされますが, 現在のリクエストは古いキャッシュデータを返します。Q prerenderとSWRは一緒に使えますか?
A はい。
{ prerender: true, swr: 3600 }はビルド時に事前レンダリングし, その後ISRキャッシング戦略で更新することを示します。Q ISRキャッシュはどこに保存されますか?
A Nitroプリセットに依存します。Node.jsプリセットはメモリまたはファイルシステムに保存, VercelプリセットはVercel KVを使用, CloudflareはKVストレージを使用します。
Q CSRページはSSR時に何を返しますか?
A 空のHTMLテンプレート (
<div id="__nuxt"></div>とJSバンドルのみを含む)を返します。全コンテンツはクライアント側でレンダリングされます。Q 現在のページのレンダリングモードを確認するにはどうすればよいですか?
A ソースコードを表示 (Ctrl+U)—コンテンツがあればSSR/SSG/ISR, 空ならCSRです。レスポンスヘッダーの
x-nuxt-render-modeもレンダリングモードを示します。Q 数百万商品ページのISRキャッシュはストレージを埋め尽くしませんか?
A 人気商品は自然にキャッシュに残り, 不人気なものは期限切れ後に自動的に削除されます。100万ページすべてを事前キャッシュする必要はなく, アクセスされたものだけがキャッシュされます。
📖 まとめ
routeRulesで各ルートに個別のレンダリング戦略を設定:SSR/CSR/SSG/ISR/ESR- ISR (swr)は最適なバランスを提供:キャッシュHIT時はSSGと同様に高速, 期限後の自動更新でデータ鮮度を確保
- ESRはCDNエッジでレンダリングし, 世界中のユーザーに最低レイテンシを提供
- MegaShopハイブリッド戦略:ホーム (SSG)+ 商品 (ISR)+ 検索 (SSR)+ 管理画面 (CSR)
- 数百万商品ページには, ISRはアクセスベースキャッシングとイベント駆動無効化を組み合わせ, 全ページの事前キャッシュは不要
📝 練習問題
- 基本問題 (難易度:⭐):
routeRulesを設定してホームページの事前レンダリングと管理画面のCSRを有効にし, 「ソースを表示」で2つのモードの出力の違いを確認してください。 - 応用問題 (難易度:⭐⭐):商品一覧にISR (swr: 60)を設定し, 60秒後にページをリフレッシュしてデータが更新されるか確認してください。
- チャレンジ (難易度:⭐⭐⭐):ISRキャッシュ無効化APIを実装してください。商品データが更新されたら
/api/invalidate-cacheを呼び出して対応キャッシュを無効化し, 次回リクエストで新しいデータが取得されることを確認してください。
---|



