0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

【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は以下を検出すると自動的に動的レンダリングに切り替わります:

// パターン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' });

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回: エラーハンドリング・メタデータ・実践パターン(近日公開)
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?