【Next.js完全ガイド Vol.2】3つのレンダリング戦略を完全理解する

に公開3

【Next.js完全ガイド Vol.2】3つのレンダリング戦略を完全理解する

はじめに

この記事は「Next.js完全ガイド」シリーズの第2回です。

📚 シリーズ構成

前回のおさらい

前回は、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でのデータ取得方法を詳しく見ていきます。

お楽しみに!


シリーズ記事

Discussion

Honey32Honey32

動的レンダリングをオプトインする方法としては、 connection() 関数もありますね。

「クッキーを取得したら、結果的に動的になった 」のような使い方の他の選択肢と違って、「クッキーとか特定の機能の結果としてでなく、明示的に動的にしたい」場合にはこちらがより分かりやすいと思います。

https://nextjs.org/docs/app/api-reference/functions/connection

Honey32Honey32

良いですね!コード例を使った説明もわかりやすいです!