✨
【Next.js完全ガイド Vol.2】3つのレンダリング戦略を完全理解する
【Next.js完全ガイド Vol.2】3つのレンダリング戦略を完全理解する
はじめに
この記事は「Next.js完全ガイド」シリーズの第2回です。
📚 シリーズ構成
- 第1回: Next.js 15最新情報 & アーキテクチャの基礎
- 第2回: レンダリング戦略の完全理解 ← 今ここ
- 第3回: データフェッチング戦略
- 第4回: キャッシング戦略
- 第5回: エラーハンドリング・メタデータ・実践パターン
前回のおさらい
前回は、Next.js 15の最新機能とアーキテクチャの基礎を学びました:
- Next.js 15の主要な新機能(React 19、キャッシング変更、Turbopack)
- Server ComponentsとClient Componentsの仕組み
- RSC Payloadの役割
- ミドルウェアによるリクエスト制御
この記事で学べること
- 静的レンダリング・動的レンダリング・ストリーミングの違い
- 各レンダリング戦略の使い分け
- コンポーネント単位でのレンダリング方式の決定方法
- 実践的なECサイトでの実装例
1. Next.jsの3つのレンダリング戦略
Next.jsは3つの主要なレンダリング戦略をサポートしており、それぞれが異なるユースケースに最適化されています。
┌─────────────────────────────────────┐
│ 1. 静的レンダリング │
│ ビルド時に生成 │
│ 全ユーザーに同じコンテンツ │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 2. 動的レンダリング │
│ リクエストごとに生成 │
│ ユーザーごとに異なるコンテンツ │
└─────────────────────────────────────┐
┌─────────────────────────────────────┐
│ 3. ストリーミング │
│ 段階的にコンテンツを配信 │
│ できた部分から順次表示 │
└─────────────────────────────────────┘
2. 静的レンダリング (Static Rendering)
概念
ビルド時(デプロイ前)に一度だけページを作って、全てのユーザーに同じものを見せる方式です。
レストランの例え
お弁当屋さん = 静的レンダリング
- 朝のうちに100個のお弁当を作る
- お客さんが来たら、すでに作ってあるものを渡す
- 超速い!全員同じお弁当
オーダーメイドレストラン = 動的レンダリング
- お客さんが来る
- 注文を聞く
- その場で料理を作る
- 時間がかかるけど、個別対応
適したユースケース
- ✅ 企業サイト(会社概要、サービス紹介)
- ✅ ブログ記事
- ✅ ドキュメント・マニュアル
- ✅ 商品カタログ
- ✅ ランディングページ
- ✅ FAQページ
実装例
// app/about/page.js
export default function AboutPage() {
return (
<div>
<h1>会社概要</h1>
<p>私たちは...</p>
</div>
);
}
// このページは自動的に静的レンダリングされる
メリット
- ⚡ 超高速(数ミリ秒)
- 💰 サーバー負荷が低い
- 🌍 CDNでキャッシュ可能
- 💵 コストが安い
動作フロー
【ビルド時】
npm run build を実行
↓
Next.jsが全ページのHTMLを生成
- /about → about.html
- /blog/post-1 → blog/post-1.html
↓
【ユーザーアクセス時】
すでに作ってあるHTMLをそのまま返す
↓
超高速!⚡
3. 動的レンダリング (Dynamic Rendering)
概念
リクエスト時に各ユーザー向けにルートがレンダリングされます。ユーザーにパーソナライズされたデータを持つ場合に最適です。
適したユースケース
- ✅ ダッシュボード(ユーザーごとに違う)
- ✅ ショッピングカート
- ✅ 検索結果ページ
- ✅ リアルタイム株価表示
- ✅ チャット画面
- ✅ ユーザープロフィールページ
実装例
// app/dashboard/page.js
import { cookies } from 'next/headers';
export default async function Dashboard() {
// cookies()を使うと自動的に動的レンダリングになる
const cookieStore = cookies();
const userId = cookieStore.get('user-id');
const userData = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json());
return (
<div>
<h1>こんにちは、{userData.name}さん</h1>
<p>あなたの残高: ¥{userData.balance}</p>
</div>
);
}
動作フロー
【ユーザーアクセス時】
ユーザーが /dashboard にアクセス
↓
誰がアクセスしたか確認(クッキー確認)
↓
データベースからユーザー情報取得
↓
その場でHTMLを生成
↓
返す(数百ミリ秒)
動的レンダリングの判定方法
Next.jsでは、以下の2つの方法で動的レンダリングになります:
1. 明示的にオプトインする(推奨)
// connection()を使って明示的に動的レンダリングを指定
import { connection } from 'next/server';
export default async function Dashboard() {
await connection(); // これだけで動的レンダリングになる
const userData = await fetchUserData();
return (
<div>
<h1>ダッシュボード</h1>
<UserData data={userData} />
</div>
);
}
メリット:
- 意図が明確(「このページは動的にしたい」という意思表示)
- クッキーやヘッダーを使わなくても動的にできる
- コードの可読性が向上
2. 特定の機能を使った結果として動的になる
// パターン1: cookies()の使用
import { cookies } from 'next/headers';
const cookieStore = cookies();
// パターン2: headers()の使用
import { headers } from 'next/headers';
const headersList = headers();
// パターン3: searchParamsの使用
export default function SearchPage({ searchParams }) {
const query = searchParams.q;
}
// パターン4: キャッシュされていないfetch
fetch(url, { cache: 'no-store' });
connection() vs その他の方法
// ❌ 意図が不明確:cookiesを使っているから結果的に動的
import { cookies } from 'next/headers';
export default async function Page() {
const cookieStore = cookies(); // 本当はクッキー取得が目的?動的にしたいだけ?
return <div>コンテンツ</div>;
}
// ✅ 意図が明確:明示的に動的レンダリングを指定
import { connection } from 'next/server';
export default async function Page() {
await connection(); // 「このページは動的にする」という意図が明確
return <div>コンテンツ</div>;
}
使い分けの例
// ケース1: クッキーを実際に使う場合
import { cookies } from 'next/headers';
export default async function Page() {
const cookieStore = cookies();
const userId = cookieStore.get('user-id'); // クッキーが必要
const user = await fetchUser(userId);
return <div>こんにちは、{user.name}さん</div>;
}
// ケース2: クッキーは使わないが動的にしたい場合
import { connection } from 'next/server';
export default async function Page() {
await connection(); // 動的レンダリングを明示
const liveData = await fetchLiveData(); // リアルタイムデータ取得
return <div>現在の値: {liveData.value}</div>;
}
4. ストリーミング (Streaming)
概念
ページ全体が完成するのを待たずに、できた部分から順番にユーザーに見せていく技術です。
動画配信の例え
【従来の方式】
映画全体(2GB)をダウンロード完了
↓
ダウンロード完了まで真っ白な画面...⏳
↓
やっと再生開始!
【ストリーミング方式】
最初の5分をダウンロード
↓
すぐに再生開始!🎬
↓
見ている間に続きをダウンロード
↓
途切れずに視聴できる✨
実装例
// app/products/page.js
import { Suspense } from 'react';
export default async function ProductsPage() {
return (
<div>
{/* すぐに表示できる部分 */}
<h1>商品一覧</h1>
<nav>カテゴリーメニュー</nav>
{/* 遅い部分は Suspense で包む */}
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<CustomerReviews />
</Suspense>
</div>
);
}
// 重いデータ取得
async function ProductList() {
const products = await fetch('https://api.example.com/products')
.then(r => r.json());
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
async function CustomerReviews() {
const reviews = await fetch('https://api.example.com/reviews')
.then(r => r.json());
return (
<div className="reviews">
{reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
);
}
ユーザー体験のタイムライン
0ms: タイトル・ナビ表示 + スケルトン表示 ✨
ユーザーはすぐに何かを見れる!
500ms: ProductList表示 ✨
商品が表示され始める
1500ms: CustomerReviews表示 ✨
レビューも表示完了
完了!全てのコンテンツが揃った
スケルトンUIの実装
// ProductsSkeleton.js
export default function ProductsSkeleton() {
return (
<div className="products-grid">
{[...Array(12)].map((_, i) => (
<div key={i} className="skeleton-card">
<div className="skeleton-image" />
<div className="skeleton-title" />
<div className="skeleton-price" />
</div>
))}
</div>
);
}
/* styles.css */
.skeleton-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
}
.skeleton-image,
.skeleton-title,
.skeleton-price {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.skeleton-image {
height: 200px;
margin-bottom: 12px;
border-radius: 4px;
}
.skeleton-title {
height: 20px;
margin-bottom: 8px;
border-radius: 4px;
}
.skeleton-price {
height: 24px;
width: 80px;
border-radius: 4px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
メリット
- 🚀 体感速度が速い
- 👆 ユーザーが早く操作できる
- ⚖️ 遅い部分が全体を遅くしない
- 📊 サーバー負荷の分散
チャンクの分割
┌─────────────────────────┐
│ ページ全体 │
├─────────────────────────┤
│ チャンク1: Header │ ← すぐ送信
├─────────────────────────┤
│ チャンク2: ProductList │ ← 500ms後に送信
├─────────────────────────┤
│ チャンク3: Reviews │ ← 1500ms後に送信
└─────────────────────────┘
5. 従来のSSR/CSRとの関係
Next.jsの3つのレンダリング戦略と、従来のSSR/CSRの関係を整理します。
関係性マップ
Next.jsのレンダリング戦略(いつレンダリングするか)
│
├─ 静的レンダリング
│ └─ SSR(ビルド時)= Static Site Generation (SSG)
│
├─ 動的レンダリング
│ └─ SSR(リクエスト時)= 従来のSSR
│
└─ ストリーミング
└─ SSR(リクエスト時 + 段階的)
= SSR + Progressive Rendering
すべてサーバーでレンダリング ↕️
Client Components
└─ クライアントサイドレンダリング (CSR)
重要なポイント
- ✅ Next.jsの3つの戦略はすべてサーバーサイドレンダリングの一種
- ✅ 違いは「いつ」と「どうやって」レンダリングするか
- ✅ Client Componentsを使えばCSRも併用可能
- ✅ 実際のアプリでは全部を組み合わせて使う
6. コンポーネントのレンダリング方式
実際のアプリケーションでは、複数のレンダリング方式を組み合わせて使用します。
コンポーネントの判定フローチャート
コンポーネントを見る
│
├─ 'use client' がある?
│ YES → Client Component (CSR + ハイドレーション)
│ NO ↓
│
├─ async function?または await を使う?
│ YES → Server Component(動的レンダリング)
│ ├─ Suspense で包まれている?
│ │ YES → ストリーミング
│ │ NO → 通常の動的レンダリング
│ NO ↓
│
├─ cookies(), headers(), searchParams を使う?
│ YES → Server Component(動的レンダリング)
│ NO ↓
│
└─ その他
└─ Server Component(静的レンダリング)
7. 実践例:ECサイトの商品ページ
実際のECサイトの商品ページで、複数のレンダリング方式を組み合わせる例を見てみましょう。
// app/products/[id]/page.js
import { Suspense } from 'react';
import ProductImageGallery from '@/components/ProductImageGallery';
import AddToCartForm from '@/components/AddToCartForm';
import ShareButtons from '@/components/ShareButtons';
// ページ全体: Server Component
export default async function ProductPage({ params }) {
const product = await db.products.findUnique({
where: { id: params.id }
});
return (
<div>
{/* ========== 静的レンダリング ========== */}
<Breadcrumbs category={product.category} />
<ProductTitle>{product.name}</ProductTitle>
<ProductDescription>{product.description}</ProductDescription>
{/* ========== 動的レンダリング(ストリーミング) ========== */}
<Suspense fallback={<PriceSkeleton />}>
<RealTimePrice productId={params.id} />
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<StockStatus productId={params.id} />
</Suspense>
{/* ========== Client Components(CSR) ========== */}
<ProductImageGallery images={product.images} />
<AddToCartForm product={product} />
<ShareButtons url={`/products/${params.id}`} />
{/* ========== 動的レンダリング(ストリーミング) ========== */}
<Suspense fallback={<ReviewsSkeleton />}>
<CustomerReviews productId={params.id} />
</Suspense>
</div>
);
}
// 静的レンダリング: パンくずリスト
function Breadcrumbs({ category }) {
return (
<nav aria-label="パンくずリスト">
<a href="/">ホーム</a> / <a href={`/category/${category}`}>{category}</a>
</nav>
);
}
// 動的レンダリング + ストリーミング: リアルタイム価格
async function RealTimePrice({ productId }) {
const price = await fetch(`https://api.prices.com/product/${productId}`, {
cache: 'no-store'
}).then(r => r.json());
return <div className="price">¥{price.current.toLocaleString()}</div>;
}
// 動的レンダリング + ストリーミング: 在庫状況
async function StockStatus({ productId }) {
const stock = await fetch(`https://api.stock.com/product/${productId}`, {
cache: 'no-store'
}).then(r => r.json());
return (
<div className={stock.available ? 'in-stock' : 'out-of-stock'}>
{stock.available ? `在庫あり(${stock.quantity}個)` : '在庫切れ'}
</div>
);
}
// 動的レンダリング + ストリーミング: カスタマーレビュー
async function CustomerReviews({ productId }) {
const reviews = await fetch(`https://api.reviews.com/product/${productId}`)
.then(r => r.json());
return (
<div className="reviews">
<h2>カスタマーレビュー</h2>
{reviews.map(review => (
<div key={review.id} className="review">
<div className="rating">{'★'.repeat(review.rating)}</div>
<p>{review.comment}</p>
</div>
))}
</div>
);
}
Client Componentsの実装
// components/ProductImageGallery.js
'use client';
import { useState } from 'react';
import Image from 'next/image';
export default function ProductImageGallery({ images }) {
const [selectedImage, setSelectedImage] = useState(0);
return (
<div className="gallery">
<div className="main-image">
<Image
src={images[selectedImage]}
alt="商品画像"
width={600}
height={600}
/>
</div>
<div className="thumbnails">
{images.map((image, index) => (
<button
key={index}
onClick={() => setSelectedImage(index)}
className={index === selectedImage ? 'active' : ''}
>
<Image src={image} alt="" width={100} height={100} />
</button>
))}
</div>
</div>
);
}
// components/AddToCartForm.js
'use client';
import { useState } from 'react';
import { addToCart } from '@/app/actions';
export default function AddToCartForm({ product }) {
const [quantity, setQuantity] = useState(1);
const [isAdding, setIsAdding] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsAdding(true);
try {
await addToCart(product.id, quantity);
alert('カートに追加しました!');
} catch (error) {
alert('エラーが発生しました');
} finally {
setIsAdding(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div className="quantity-selector">
<label htmlFor="quantity">数量:</label>
<input
id="quantity"
type="number"
min="1"
max="10"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
</div>
<button type="submit" disabled={isAdding}>
{isAdding ? 'カートに追加中...' : 'カートに追加'}
</button>
</form>
);
}
// components/ShareButtons.js
'use client';
export default function ShareButtons({ url }) {
const shareOnTwitter = () => {
window.open(
`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}`,
'_blank'
);
};
const shareOnFacebook = () => {
window.open(
`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
'_blank'
);
};
const copyLink = () => {
navigator.clipboard.writeText(url);
alert('リンクをコピーしました!');
};
return (
<div className="share-buttons">
<button onClick={shareOnTwitter}>Twitterでシェア</button>
<button onClick={shareOnFacebook}>Facebookでシェア</button>
<button onClick={copyLink}>リンクをコピー</button>
</div>
);
}
レンダリング方式のまとめ表
| コンポーネント | 種類 | レンダリング方式 | 理由 |
|---|---|---|---|
| ProductPage | Server | 動的(ベース) | paramsを使用 |
| Breadcrumbs | Server | 静的 | 固定データ |
| ProductTitle | Server | 静的 | 固定データ |
| RealTimePrice | Server | 動的 + ストリーミング | リアルタイムデータ |
| StockStatus | Server | 動的 + ストリーミング | リアルタイムデータ |
| CustomerReviews | Server | 動的 + ストリーミング | 外部API |
| ProductImageGallery | Client | CSR | インタラクション |
| AddToCartForm | Client | CSR | フォーム送信 |
| ShareButtons | Client | CSR | ブラウザAPI使用 |
実行フロー(タイムライン)
【ビルド時】
- 静的な部分を事前コンパイル
- Client Components のJavaScriptをバンドル
【リクエスト時】
0ms: リクエスト受信
↓
50ms: Server Components を実行、初期HTML送信
✅ パンくずリスト
✅ 商品タイトル・説明
✅ 商品画像ギャラリー(初期HTML)
⏳ 価格(ローディング中)
⏳ 在庫(ローディング中)
🔘 カートに追加ボタン(初期HTML)
↓
100ms: JavaScript 読み込み完了、ハイドレーション
🔘 カートに追加ボタン(クリック可能!)
🖼️ 画像ギャラリー(スワイプ可能!)
↓
250ms: 価格データを受信、表示更新
✅ 価格表示(¥9,980)
↓
300ms: 在庫データを受信、表示更新
✅ 在庫表示(在庫あり)
↓
1000ms: レビューデータを受信、表示更新
✅ カスタマーレビュー表示
↓
完了!全てのコンテンツが揃った
8. レンダリング戦略の選び方
判断フローチャート
ページ/コンポーネントを作る
↓
データは必要?
├─ NO → 静的レンダリング
└─ YES ↓
データは変わる?
├─ ほぼ変わらない → 静的レンダリング + ISR
└─ 頻繁に変わる ↓
ユーザーごとに違う?
├─ YES → 動的レンダリング
└─ NO ↓
重い処理がある?
├─ YES → ストリーミング
└─ NO → 動的レンダリング
インタラクションが必要?
└─ YES → Client Component
具体例で判断
Q: ブログ記事を表示したい
↓
A: データは必要? YES
データは変わる? ほぼ変わらない
→ 静的レンダリング + ISR (revalidate: 3600)
Q: ユーザーダッシュボードを作りたい
↓
A: データは必要? YES
データは変わる? 頻繁に変わる
ユーザーごとに違う? YES
→ 動的レンダリング
Q: 画像ギャラリーを作りたい
↓
A: インタラクションが必要? YES
→ Client Component
Q: 商品一覧を表示したい(在庫表示あり)
↓
A: データは必要? YES
重い処理がある? YES(在庫確認)
→ ストリーミング(商品情報は先に表示、在庫は後から)
まとめ
この記事では、Next.jsの3つのレンダリング戦略を詳しく学びました:
- ✅ 静的レンダリング: ビルド時に生成、超高速
- ✅ 動的レンダリング: リクエストごとに生成、パーソナライズ可能
- ✅ ストリーミング: 段階的に配信、体感速度が速い
- ✅ コンポーネント単位での使い分け方法
- ✅ 実践的なECサイトでの実装例
次回は「データフェッチング戦略」について解説します。Server Components、Route Handlers、Server Actions、Client Componentsでのデータ取得方法を詳しく見ていきます。
お楽しみに!
シリーズ記事
- 第1回: Next.js 15最新情報 & アーキテクチャの基礎
- 第2回: レンダリング戦略の完全理解 ← 今ここ
- 第3回: データフェッチング戦略
- 第4回: キャッシング戦略
- 第5回: エラーハンドリング・メタデータ・実践パターン
Discussion
動的レンダリングをオプトインする方法としては、
connection()関数もありますね。「クッキーを取得したら、結果的に動的になった 」のような使い方の他の選択肢と違って、「クッキーとか特定の機能の結果としてでなく、明示的に動的にしたい」場合にはこちらがより分かりやすいと思います。
👍 ありがとうございます。更新しました。
良いですね!コード例を使った説明もわかりやすいです!