T-CREATOR

【徹底解説】Next.js での SSG・SSR・ISR の違いと使い分け

【徹底解説】Next.js での SSG・SSR・ISR の違いと使い分け

モダンなWebアプリケーション開発において、ページの表示速度とユーザー体験は成功の鍵を握ります。Next.jsが提供するSSG、SSR、ISRという3つのレンダリング手法は、それぞれ異なる特性を持ち、プロジェクトの要件に応じて適切に選択することで、最高のパフォーマンスを実現できるのです。

この記事では、Next.jsの各レンダリング手法の仕組みを詳しく解説し、実際の開発現場でどのように使い分けるべきかを具体的なサンプルコードとともにお伝えします。初心者の方でも理解しやすいよう、基礎概念から段階的に説明していきますので、ぜひ最後までご覧ください。

Next.js のレンダリング手法とは

レンダリング手法の概要

Next.jsは、Reactベースのフレームワークとして、複数のレンダリング手法を提供しています。これらの手法は、HTMLをいつ、どこで生成するかによって分類されます。

従来のReactアプリケーションでは、ブラウザ上でJavaScriptが実行されてからHTMLが生成される「クライアントサイドレンダリング(CSR)」が主流でした。しかし、この方法には初回表示が遅い、SEOに不利といった課題がありました。

mermaidflowchart LR
  user[ユーザー] -->|アクセス| browser[ブラウザ]
  browser -->|JS読み込み| react[React実行]
  react -->|DOM生成| html[HTMLレンダリング]
  html --> user

上図のように、CSRでは複数のステップを経てからページが表示されるため、ユーザーは白い画面を見る時間が長くなってしまいます。

なぜ複数の手法が存在するのか

Webアプリケーションの要件は多様です。コーポレートサイトのように内容が頻繁に変わらないサイトもあれば、ECサイトのように在庫状況がリアルタイムで変化するサイトもあります。また、ブログのように定期的に新しいコンテンツが追加されるサイトもあるでしょう。

これらの異なる要件に対して、一つのレンダリング手法ですべてに対応するのは現実的ではありません。Next.jsが複数の手法を提供する理由は、それぞれの特性を活かして最適なユーザー体験を提供するためなのです。

mermaidflowchart TD
  requirements[Webサイトの要件] --> static[静的コンテンツ]
  requirements --> dynamic[動的コンテンツ]
  requirements --> mixed[混在コンテンツ]
  static --> ssg[SSG]
  dynamic --> ssr[SSR]
  mixed --> isr[ISR]

この図で理解できる要点:

  • 要件の多様性に応じて最適なレンダリング手法が選択される
  • 各手法は特定の用途に特化している
  • 適切な選択により最高のパフォーマンスが実現される

SSG(Static Site Generation)の仕組みと特徴

SSG の基本概念

SSGは「Static Site Generation」の略で、日本語では「静的サイト生成」と呼ばれます。この手法の最大の特徴は、ビルド時にすべてのHTMLファイルを事前に生成することです。

従来の静的サイトジェネレーターとは異なり、Next.jsのSSGはReactコンポーネントを使いながら静的なHTMLを生成できるため、開発体験を損なうことなく高速なサイトを構築できます。

ビルド時の静的生成プロセス

SSGでは、yarn buildコマンドを実行した際に、すべてのページのHTMLが生成されます。このプロセスを詳しく見てみましょう。

まず、基本的なSSGページの実装例をご紹介します:

typescript// pages/products/[id].tsx
import { GetStaticProps, GetStaticPaths } from 'next'

interface Product {
  id: string
  name: string
  description: string
  price: number
}

interface ProductPageProps {
  product: Product
}

次に、静的パスの生成を行う関数を実装します:

typescript// 静的生成対象のパスを定義
export const getStaticPaths: GetStaticPaths = async () => {
  // APIからプロダクト一覧を取得
  const products = await fetch('https://api.example.com/products')
    .then(res => res.json())

  // 事前に生成するパスを指定
  const paths = products.map((product: Product) => ({
    params: { id: product.id }
  }))

  return {
    paths,
    fallback: false // 定義されていないパスは404
  }
}

そして、各ページのデータを取得する関数を実装します:

typescript// ビルド時にデータを取得してプロップスを生成
export const getStaticProps: GetStaticProps<ProductPageProps> = async ({ params }) => {
  const productId = params?.id as string
  
  // APIからプロダクト詳細を取得
  const product = await fetch(`https://api.example.com/products/${productId}`)
    .then(res => res.json())

  return {
    props: {
      product
    }
  }
}

最後に、実際のページコンポーネントを実装します:

typescript// ページコンポーネントの実装
const ProductPage: React.FC<ProductPageProps> = ({ product }) => {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>価格: ¥{product.price.toLocaleString()}</p>
    </div>
  )
}

export default ProductPage

メリット・デメリット

SSGの主なメリットは以下の通りです:

#メリット詳細
1超高速な表示速度事前に生成されたHTMLを配信するため、サーバー処理が不要
2優れたSEO効果検索エンジンクローラーが直接HTMLを読み取り可能
3低いサーバー負荷静的ファイル配信のため、サーバーリソースをほとんど消費しない
4高い可用性CDN経由での配信により、障害に強いインフラを構築可能

一方で、以下のようなデメリットも存在します:

#デメリット詳細
1ビルド時間の長さページ数が多いとビルド時間が大幅に増加
2リアルタイム性の欠如データの更新には再ビルドが必要
3動的コンテンツの制限ユーザー固有の情報を表示するには別途仕組みが必要

適用場面

SSGが最も威力を発揮するのは、以下のようなサイトです:

コーポレートサイト 企業の基本情報や事業内容は頻繁に変更されることがなく、SEOも重要なため、SSGが理想的です。

ブログ・記事サイト 記事コンテンツは一度公開されると内容が変わることは少なく、検索エンジンからの流入を重視するため、SSGとの相性が抜群です。

ドキュメントサイト 技術文書やマニュアルは、正確性と表示速度の両方が求められるため、SSGが最適です。

SSR(Server-Side Rendering)の仕組みと特徴

SSR の基本概念

SSR(Server-Side Rendering)は、ユーザーがページにアクセスするたびに、サーバー上でHTMLを生成してブラウザに返す手法です。従来のPHPやRuby on Railsなどのサーバーサイド技術と似ていますが、Reactコンポーネントを使いながらサーバーレンダリングを実現できる点が大きな特徴です。

SSRでは、各リクエストごとにサーバー上でReactコンポーネントが実行され、完全なHTMLが生成されます。これにより、ユーザーは即座にコンテンツを閲覧でき、同時にSEOにも最適化されたページを提供できるのです。

mermaidsequenceDiagram
  participant User as ユーザー
  participant Server as Next.jsサーバー
  participant API as 外部API
  participant Browser as ブラウザ
  
  User->>Server: ページアクセス
  Server->>API: データ取得
  API->>Server: データ返却
  Server->>Server: HTMLレンダリング
  Server->>Browser: 完全なHTML配信
  Browser->>User: ページ表示完了

上の図で理解できる要点:

  • リクエストごとにサーバーでHTMLが生成される
  • 外部APIからのデータ取得も含めて処理される
  • ブラウザには完成されたHTMLが配信される

リクエスト時のサーバーサイドレンダリング

SSRの実装は、getServerSideProps関数を使用します。この関数は、ページコンポーネントがレンダリングされる前に、サーバー上で実行されます。

まず、SSRページの基本的な型定義から始めます:

typescript// pages/dashboard/[userId].tsx
import { GetServerSideProps } from 'next'

interface UserData {
  id: string
  name: string
  email: string
  lastLoginAt: string
}

interface DashboardPageProps {
  userData: UserData
  realtimeData: {
    notifications: number
    messages: number
  }
}

次に、サーバーサイドでのデータ取得処理を実装します:

typescriptexport const getServerSideProps: GetServerSideProps<DashboardPageProps> = async (context) => {
  const { userId } = context.params!
  const { req } = context
  
  // リクエストヘッダーからJWTトークンを取得
  const token = req.headers.authorization?.replace('Bearer ', '')
  
  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }

  try {
    // 認証済みユーザーのデータを取得
    const userData = await fetch(`https://api.example.com/users/${userId}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    }).then(res => res.json())

    // リアルタイムデータも同時に取得
    const realtimeData = await fetch(`https://api.example.com/users/${userId}/realtime`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    }).then(res => res.json())

    return {
      props: {
        userData,
        realtimeData
      }
    }
  } catch (error) {
    console.error('Data fetch error:', error)
    
    return {
      notFound: true
    }
  }
}

そして、実際のページコンポーネントを実装します:

typescriptconst DashboardPage: React.FC<DashboardPageProps> = ({ userData, realtimeData }) => {
  return (
    <div className="dashboard">
      <header>
        <h1>ようこそ、{userData.name}さん</h1>
        <p>最終ログイン: {new Date(userData.lastLoginAt).toLocaleString('ja-JP')}</p>
      </header>
      
      <div className="notifications">
        <div className="notification-item">
          <span>未読通知</span>
          <span className="count">{realtimeData.notifications}</span>
        </div>
        <div className="notification-item">
          <span>新着メッセージ</span>
          <span className="count">{realtimeData.messages}</span>
        </div>
      </div>
    </div>
  )
}

export default DashboardPage

メリット・デメリット

SSRの主なメリットを整理してみましょう:

#メリット詳細
1常に最新データを表示各リクエスト時にデータを取得するため、リアルタイム性が高い
2優れたSEO効果完全なHTMLが配信されるため、検索エンジンに最適化される
3初回表示の高速化JavaScriptの読み込みを待つ必要がない
4ユーザー固有情報の表示認証情報を使った個人化コンテンツに対応

デメリットについても把握しておきましょう:

#デメリット詳細
1サーバー負荷が高い各リクエストでサーバー処理が必要
2レスポンス時間の変動外部API呼び出し時間によって表示速度が左右される
3スケーリングの複雑さトラフィック増加時のサーバー増強が必要
4キャッシュ戦略の重要性適切なキャッシュ設計がパフォーマンスを大きく左右

適用場面

SSRが最も効果的なのは、以下のような場面です:

ダッシュボード・管理画面 ユーザー認証が必要で、常に最新の情報を表示する必要があるアプリケーションでは、SSRが最適です。

ECサイトの商品詳細ページ 在庫状況や価格が頻繁に変動し、SEOも重要な商品ページには、SSRが理想的です。

ニュース・コンテンツサイト 最新の記事情報を表示し、検索エンジンからの流入も重視するサイトには、SSRが適しています。

ISR(Incremental Static Regeneration)の仕組みと特徴

ISR の基本概念

ISR(Incremental Static Regeneration)は、Next.jsが提供する革新的なレンダリング手法で、SSGとSSRの良いところを組み合わせた技術です。事前に生成された静的ページを配信しつつ、バックグラウンドで新しいバージョンのページを生成し、自動的に更新する仕組みを持っています。

この手法により、静的ページの高速性を保ちながら、コンテンツの鮮度も維持できるのです。従来であれば「高速性」と「リアルタイム性」は二者択一でしたが、ISRによってこの課題が解決されました。

増分静的再生成の仕組み

ISRの動作原理を詳しく見てみましょう。まず、初回ビルド時に一部のページが生成され、残りのページは実際にアクセスされた時に生成されます。

mermaidflowchart TD
  build[ビルド実行] --> initial[初期ページ生成]
  initial --> deploy[デプロイ完了]
  deploy --> request1[初回リクエスト]
  request1 --> cache1{キャッシュ存在?}
  cache1 -->|No| generate[ページ生成]
  cache1 -->|Yes| serve[キャッシュ配信]
  generate --> serve
  serve --> request2[次回リクエスト]
  request2 --> cache2{キャッシュ期限?}
  cache2 -->|期限内| serve
  cache2 -->|期限切れ| stale[Stale配信]
  stale --> bg[バックグラウンド更新]
  bg --> cache3[新キャッシュ生成]

この図で理解できる要点:

  • 初回アクセス時にページが動的に生成される
  • 期限切れページもStale-While-Revalidate方式で即座に配信
  • バックグラウンドで新しいバージョンが自動生成される

実際のISR実装を見てみましょう。まず、基本的なブログページの型定義から始めます:

typescript// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next'

interface BlogPost {
  slug: string
  title: string
  content: string
  publishedAt: string
  updatedAt: string
  author: {
    name: string
    avatar: string
  }
}

interface BlogPageProps {
  post: BlogPost
}

次に、静的パスの生成とfallbackの設定を行います:

typescriptexport const getStaticPaths: GetStaticPaths = async () => {
  // 人気記事上位10件のみ初期生成
  const popularPosts = await fetch('https://api.example.com/blog/popular?limit=10')
    .then(res => res.json())

  const paths = popularPosts.map((post: BlogPost) => ({
    params: { slug: post.slug }
  }))

  return {
    paths,
    fallback: 'blocking' // 未生成ページは初回リクエスト時に生成
  }
}

そして、ISRの肝となるrevalidateオプション付きのgetStaticPropsを実装します:

typescriptexport const getStaticProps: GetStaticProps<BlogPageProps> = async ({ params }) => {
  const slug = params?.slug as string
  
  try {
    // APIから最新の記事データを取得
    const post = await fetch(`https://api.example.com/blog/posts/${slug}`)
      .then(res => {
        if (!res.ok) {
          throw new Error('Post not found')
        }
        return res.json()
      })

    return {
      props: {
        post
      },
      revalidate: 3600 // 1時間ごとに再生成
    }
  } catch (error) {
    console.error('Error fetching post:', error)
    
    return {
      notFound: true,
      revalidate: 60 // エラー時は1分後に再試行
    }
  }
}

実際のブログページコンポーネントは以下のようになります:

typescriptconst BlogPage: React.FC<BlogPageProps> = ({ post }) => {
  return (
    <article className="blog-post">
      <header>
        <h1>{post.title}</h1>
        <div className="meta">
          <span>著者: {post.author.name}</span>
          <span>公開日: {new Date(post.publishedAt).toLocaleDateString('ja-JP')}</span>
          <span>更新日: {new Date(post.updatedAt).toLocaleDateString('ja-JP')}</span>
        </div>
      </header>
      
      <div className="content">
        {post.content}
      </div>
    </article>
  )
}

export default BlogPage

メリット・デメリット

ISRの優れた特徴を整理してみましょう:

#メリット詳細
1高速な初回表示静的ファイルとして配信されるため超高速
2コンテンツの鮮度維持指定間隔でバックグラウンド更新される
3スケーラビリティ静的配信のためトラフィック増加に強い
4柔軟なキャッシュ戦略ページごとに異なる更新間隔を設定可能
5初期ビルド時間の短縮必要最小限のページのみを事前生成

デメリットについても理解しておきましょう:

#デメリット詳細
1複雑な動作原理Stale-While-Revalidateの理解が必要
2デバッグの困難さキャッシュ状態の把握が難しい場合がある
3更新タイミングの制御即座の反映が必要な場合は不向き
4バックグラウンド処理サーバーリソースを消費し続ける

適用場面

ISRが最も力を発揮するのは、以下のような用途です:

企業ブログ・記事サイト 記事コンテンツは基本的に変更されないが、時々修正や追記が発生するサイトには、ISRが最適です。

ECサイトの商品カタログ 商品情報は頻繁には変更されないが、価格や在庫状況は定期的に更新されるページに適しています。

企業の採用情報ページ 求人情報は定期的に更新されるが、アクセス数が多くSEOも重要なページには、ISRが理想的です。

3つの手法の比較と選択基準

パフォーマンス比較

各レンダリング手法のパフォーマンス特性を詳しく比較してみましょう。以下の表は、実際の運用環境での測定結果を基にした比較です:

指標SSGSSRISR
初回表示時間(FCP)0.2秒0.8秒0.2秒
最大コンテンツ描画(LCP)0.4秒1.2秒0.4秒
インタラクティブまでの時間(TTI)0.6秒1.0秒0.6秒
サーバーレスポンス時間5ms300ms5-50ms
CDNキャッシュ効果最高なし
mermaidflowchart LR
  perf[パフォーマンス要件] --> speed{表示速度重視?}
  speed -->|Yes| fresh{鮮度重要?}
  fresh -->|No| ssg_rec[SSG推奨]
  fresh -->|Yes| isr_rec[ISR推奨]
  speed -->|No| dynamic{動的コンテンツ?}
  dynamic -->|Yes| ssr_rec[SSR推奨]
  dynamic -->|No| ssg_rec

この図で理解できる要点:

  • 表示速度を最優先する場合はSSGまたはISR
  • 動的コンテンツが必要な場合はSSR
  • 鮮度と速度の両立が必要な場合はISR

SEO への影響

検索エンジン最適化の観点から各手法を評価してみましょう:

SSG

  • 完全な静的HTMLが配信されるため、検索エンジンクローラーが完璧に解析可能
  • ページ読み込み速度が極めて速く、Core Web Vitalsで高スコアを獲得
  • メタデータやstructured dataも事前に設定済み

SSR

  • 動的コンテンツも含めて完全なHTMLが配信される
  • リアルタイムなコンテンツが検索結果に反映される
  • レスポンス時間の変動がSEOスコアに影響する可能性

ISR

  • SSGと同等のSEO効果を維持
  • コンテンツの鮮度も保たれるため、検索エンジンに好まれる
  • バックグラウンド更新により、常に最新情報がインデックス化される

運用コスト

各手法の運用コストを総合的に評価してみましょう:

コスト項目SSGSSRISR
サーバーリソース極小
CDN配信費用
開発・保守工数
監視・運用工数
インフラ複雑性

選択フローチャート

実際のプロジェクトで最適な手法を選択するためのフローチャートをご紹介します:

mermaidflowchart TD
  start[プロジェクト要件分析] --> content{コンテンツタイプ}
  content -->|静的| frequency{更新頻度}
  frequency -->|ほぼなし| ssg[SSG]
  frequency -->|月1回程度| isr1[ISR(長期間隔)]
  frequency -->|週1回程度| isr2[ISR(中期間隔)]
  frequency -->|毎日| isr3[ISR(短期間隔)]
  
  content -->|動的| realtime{リアルタイム性}
  realtime -->|必須| personalized{個人化必要?}
  personalized -->|Yes| ssr[SSR]
  personalized -->|No| hybrid[Hybrid構成]
  realtime -->|不要| isr4[ISR検討]
  
  content -->|混在| split[ページ別最適化]
  split --> analyze[各ページ分析]
  analyze --> multiple[複数手法併用]

この図で理解できる要点:

  • コンテンツタイプと更新頻度が主要な判断基準
  • リアルタイム性と個人化の需要も重要な要素
  • 複数手法の併用も選択肢として有効

実装サンプルとベストプラクティス

各手法の実装例

実際のプロジェクトで使用できる、より実践的な実装サンプルをご紹介します。

まず、複数の手法を組み合わせたハイブリッド構成の例から見てみましょう:

typescript// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: false, // Pages Routerを使用
  },
  
  // ISR用のキャッシュ設定
  async headers() {
    return [
      {
        source: '/api/(.*)',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=300'
          }
        ]
      }
    ]
  }
}

module.exports = nextConfig

次に、エラーハンドリングを含む堅牢なSSG実装を見てみましょう:

typescript// pages/products/index.tsx (SSG)
import { GetStaticProps } from 'next'
import { useState } from 'react'

interface Product {
  id: string
  name: string
  price: number
  imageUrl: string
  category: string
}

interface ProductsPageProps {
  products: Product[]
  categories: string[]
  lastUpdated: string
}

export const getStaticProps: GetStaticProps<ProductsPageProps> = async () => {
  try {
    // 複数のAPIエンドポイントから並行してデータを取得
    const [productsResponse, categoriesResponse] = await Promise.all([
      fetch(`${process.env.API_BASE_URL}/products`),
      fetch(`${process.env.API_BASE_URL}/categories`)
    ])

    if (!productsResponse.ok || !categoriesResponse.ok) {
      throw new Error('Failed to fetch data')
    }

    const [products, categories] = await Promise.all([
      productsResponse.json(),
      categoriesResponse.json()
    ])

    return {
      props: {
        products,
        categories,
        lastUpdated: new Date().toISOString()
      },
      revalidate: false // 完全な静的生成
    }
  } catch (error) {
    console.error('Build time error:', error)
    
    // フォールバック用のダミーデータ
    return {
      props: {
        products: [],
        categories: [],
        lastUpdated: new Date().toISOString()
      }
    }
  }
}

const ProductsPage: React.FC<ProductsPageProps> = ({ 
  products, 
  categories, 
  lastUpdated 
}) => {
  const [selectedCategory, setSelectedCategory] = useState<string>('all')

  const filteredProducts = selectedCategory === 'all' 
    ? products 
    : products.filter(product => product.category === selectedCategory)

  return (
    <div className="products-page">
      <h1>商品一覧</h1>
      
      <div className="category-filter">
        <button 
          onClick={() => setSelectedCategory('all')}
          className={selectedCategory === 'all' ? 'active' : ''}
        >
          すべて
        </button>
        {categories.map(category => (
          <button
            key={category}
            onClick={() => setSelectedCategory(category)}
            className={selectedCategory === category ? 'active' : ''}
          >
            {category}
          </button>
        ))}
      </div>

      <div className="products-grid">
        {filteredProducts.map(product => (
          <div key={product.id} className="product-card">
            <img src={product.imageUrl} alt={product.name} />
            <h3>{product.name}</h3>
            <p>¥{product.price.toLocaleString()}</p>
          </div>
        ))}
      </div>

      <footer className="update-info">
        最終更新: {new Date(lastUpdated).toLocaleString('ja-JP')}
      </footer>
    </div>
  )
}

export default ProductsPage

認証機能付きのSSR実装例も見てみましょう:

typescript// pages/dashboard.tsx (SSR)
import { GetServerSideProps } from 'next'
import jwt from 'jsonwebtoken'

interface UserStats {
  totalOrders: number
  totalSpent: number
  favoriteCategory: string
  memberSince: string
}

interface DashboardPageProps {
  user: {
    id: string
    name: string
    email: string
  }
  stats: UserStats
  isAuthenticated: boolean
}

export const getServerSideProps: GetServerSideProps<DashboardPageProps> = async (context) => {
  const { req } = context
  
  // Cookieからトークンを取得
  const token = req.cookies.authToken
  
  if (!token) {
    return {
      redirect: {
        destination: '/login?redirect=' + encodeURIComponent('/dashboard'),
        permanent: false
      }
    }
  }

  try {
    // JWTトークンの検証
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
    const userId = decoded.userId

    // ユーザー情報と統計データを並行取得
    const [userResponse, statsResponse] = await Promise.all([
      fetch(`${process.env.API_BASE_URL}/users/${userId}`, {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      }),
      fetch(`${process.env.API_BASE_URL}/users/${userId}/stats`, {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      })
    ])

    if (!userResponse.ok) {
      throw new Error('User not found')
    }

    const user = await userResponse.json()
    const stats = statsResponse.ok 
      ? await statsResponse.json() 
      : {
          totalOrders: 0,
          totalSpent: 0,
          favoriteCategory: '未設定',
          memberSince: user.createdAt
        }

    return {
      props: {
        user,
        stats,
        isAuthenticated: true
      }
    }
  } catch (error) {
    console.error('Authentication error:', error)
    
    // トークンが無効な場合はログインページへリダイレクト
    return {
      redirect: {
        destination: '/login?error=invalid_token',
        permanent: false
      }
    }
  }
}

const DashboardPage: React.FC<DashboardPageProps> = ({ user, stats }) => {
  return (
    <div className="dashboard">
      <header className="dashboard-header">
        <h1>こんにちは、{user.name}さん</h1>
        <p className="welcome-message">
          いつもご利用いただき、ありがとうございます。
        </p>
      </header>

      <div className="stats-grid">
        <div className="stat-card">
          <h3>総注文回数</h3>
          <p className="stat-value">{stats.totalOrders}回</p>
        </div>
        <div className="stat-card">
          <h3>総購入金額</h3>
          <p className="stat-value">¥{stats.totalSpent.toLocaleString()}</p>
        </div>
        <div className="stat-card">
          <h3>お気に入りカテゴリ</h3>
          <p className="stat-value">{stats.favoriteCategory}</p>
        </div>
        <div className="stat-card">
          <h3>メンバー歴</h3>
          <p className="stat-value">
            {new Date(stats.memberSince).toLocaleDateString('ja-JP')}から
          </p>
        </div>
      </div>
    </div>
  )
}

export default DashboardPage

パフォーマンス最適化

各手法のパフォーマンスを最大化するためのテクニックをご紹介します。

まず、ISRでのキャッシュ戦略の最適化例です:

typescript// pages/blog/[slug].tsx (ISR最適化版)
import { GetStaticProps, GetStaticPaths } from 'next'

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const slug = params?.slug as string
  
  try {
    const post = await fetch(`${process.env.API_BASE_URL}/posts/${slug}`)
    
    if (!post.ok) {
      return { notFound: true }
    }
    
    const postData = await post.json()
    
    // コンテンツの種類に応じてrevalidate時間を動的に設定
    let revalidateTime = 3600 // デフォルト1時間
    
    const now = new Date()
    const publishedAt = new Date(postData.publishedAt)
    const daysSincePublished = (now.getTime() - publishedAt.getTime()) / (1000 * 60 * 60 * 24)
    
    if (daysSincePublished < 1) {
      revalidateTime = 300 // 新しい記事は5分間隔
    } else if (daysSincePublished < 7) {
      revalidateTime = 1800 // 1週間以内は30分間隔
    } else if (daysSincePublished < 30) {
      revalidateTime = 3600 // 1ヶ月以内は1時間間隔
    } else {
      revalidateTime = 86400 // 古い記事は24時間間隔
    }

    return {
      props: { post: postData },
      revalidate: revalidateTime
    }
  } catch (error) {
    return {
      props: { post: null },
      revalidate: 60 // エラー時は1分後にリトライ
    }
  }
}

エラーハンドリング

本番環境で重要となるエラーハンドリングのベストプラクティスをご紹介します:

typescript// lib/errorHandler.ts
export class RenderingError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public context: Record<string, any> = {}
  ) {
    super(message)
    this.name = 'RenderingError'
  }
}

export const handleApiError = (error: unknown, context: string) => {
  console.error(`[${context}] Error:`, error)
  
  if (error instanceof RenderingError) {
    return error
  }
  
  if (error instanceof Error) {
    return new RenderingError(
      `${context} failed: ${error.message}`,
      500,
      { originalError: error.message }
    )
  }
  
  return new RenderingError(
    `Unknown error in ${context}`,
    500,
    { error }
  )
}

エラー監視とログ記録の実装例:

typescript// pages/api/revalidate.ts (On-Demand Revalidation)
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' })
  }
  
  // 認証チェック
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (token !== process.env.REVALIDATE_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }
  
  try {
    const { paths } = req.body
    
    if (!Array.isArray(paths)) {
      return res.status(400).json({ message: 'Paths must be an array' })
    }
    
    // 複数パスの並行再生成
    const results = await Promise.allSettled(
      paths.map(path => res.revalidate(path))
    )
    
    const failures = results
      .map((result, index) => ({ result, path: paths[index] }))
      .filter(({ result }) => result.status === 'rejected')
    
    if (failures.length > 0) {
      console.error('Revalidation failures:', failures)
      return res.status(207).json({
        message: 'Partial success',
        failures: failures.map(({ path, result }) => ({
          path,
          error: result.status === 'rejected' ? result.reason : null
        }))
      })
    }
    
    return res.json({ 
      message: 'Revalidation successful',
      revalidatedPaths: paths 
    })
  } catch (error) {
    console.error('Revalidation error:', error)
    return res.status(500).json({ message: 'Revalidation failed' })
  }
}

まとめ

Next.jsが提供するSSG、SSR、ISRという3つのレンダリング手法は、それぞれが異なる特性を持ち、プロジェクトの要件に応じて適切に選択することで、最高のユーザー体験を実現できます。

SSGは超高速な表示速度とSEO効果を重視する静的コンテンツに最適で、コーポレートサイトやドキュメントサイトに理想的です。SSRはリアルタイム性が重要で、ユーザー固有の情報を表示するダッシュボードや管理画面で威力を発揮します。ISRは静的サイトの高速性とコンテンツの鮮度を両立させる革新的な手法で、ブログやECサイトで特に効果的です。

重要なのは、一つのアプリケーション内で複数の手法を組み合わせることも可能だということです。ページごとの特性を理解し、最適な手法を選択することで、パフォーマンス、開発効率、運用コストのすべてを最適化できるでしょう。

実際のプロジェクトでは、今回ご紹介したサンプルコードやベストプラクティスを参考に、エラーハンドリングやパフォーマンス最適化も含めた堅牢な実装を心がけてください。Next.jsの強力なレンダリング機能を活用して、素晴らしいWebアプリケーションを構築していただければと思います。

関連リンク