Next.js ISR(Incremental Static Regeneration)とは?仕組みと実装方法を徹底解説

React / Next.js

Next.js ISR(Incremental Static Regeneration)とは?仕組みと実装方法を徹底解説

1. ISRの原因と背景:なぜISRが必要なのか

Next.jsを使ったWebアプリケーション開発では、「静的生成(Static Generation)」と「サーバーサイドレンダリング(SSR)」という2つの主要なレンダリング方式があります。

しかし、これらの従来の方法には課題がありました:

  • 静的生成の課題:一度ビルドされたページはその後の更新に対応できず、コンテンツの変更があるたびに全体を再ビルドする必要があります
  • SSRの課題:すべてのページがリクエストごとにレンダリングされるため、サーバー負荷が高く、ページ表示速度が低下します

このジレンマを解決するために、Next.js 9.5で導入されたのがISR(Incremental Static Regeneration)です。ISRは、静的生成の高速性とSSRの動的更新性を両立させる革新的なアプローチです。

ISRとは何か

ISRは「段階的静的再生成」と訳されます。ISRの基本的な概念は:

  • ページは事前に静的に生成されます
  • 指定した時間間隔で、バックグラウンドでそのページを自動的に再生成します
  • ユーザーは常に高速な静的ページを受け取ります
  • 定期的に最新のコンテンツに更新されます

2. ISRの仕組み:詳しく解説

従来の方法とISRの比較

従来の静的生成(SSG):

  • ビルド時にすべてのページを生成
  • デプロイ後はコンテンツが固定
  • 変更時は再ビルド・再デプロイが必要

従来のSSR:

  • リクエストごとにページを生成
  • 常に最新のコンテンツを表示可能
  • サーバー負荷が高い

ISR:

  • ビルド時にページを生成
  • 設定した時間間隔(revalidate)で自動再生成
  • その間はスタティックなキャッシュを利用
  • サーバー負荷とコンテンツの鮮度のバランスが取れている

ISRのタイムライン

具体的な流れは以下の通りです:

  1. ビルド時:すべての静的ページを生成
  2. 初回アクセス:キャッシュされた静的ページを返す
  3. revalidateで指定した時間経過後:バックグラウンドで再生成が開始
  4. 再生成中:古いページがユーザーに返される(ステイル・ホイル・リバリデート)
  5. 再生成完了:新しいページがキャッシュされ、次のアクセスから新しいコンテンツが返される

3. ISRの解決手順と実装方法

ステップ1:getStaticPropsでrevalidateを設定

ISRを使用するには、getStaticPropsでrevalidateプロパティを設定します。

// pages/blog/[id].js
import { getAllPostIds, getPostData } from '../lib/posts'

export async function getStaticProps({ params }) {
  const postData = await getPostData(params.id)
  
  return {
    props: {
      postData
    },
    // 3600秒(1時間)ごとに再生成
    revalidate: 3600
  }
}

export async function getStaticPaths() {
  const ids = await getAllPostIds()
  
  return {
    paths: ids.map(id => ({
      params: { id: id.toString() }
    })),
    // true: 未生成ページへのアクセス時にオンデマンド生成
    // false: 404を返す
    fallback: 'blocking'
  }
}

export default function Post({ postData }) {
  return (
    

{postData.title}

{postData.date}

) }

ステップ2:revalidateの値を決定

revalidateの値は、コンテンツの更新頻度に応じて調整します:

  • 10秒:リアルタイム性が重要なニュースサイト
  • 60秒:頻繁に更新されるブログやECサイト
  • 3600秒(1時間):定期的な更新で十分なコンテンツ
  • 86400秒(1日):更新頻度が低いコンテンツ

ステップ3:On-Demand ISRの活用

特定のページを即座に再生成する必要がある場合、On-Demand ISRを使用します。

// pages/api/revalidate.js
export default async function handler(req, res) {
  // シークレットトークンで認証
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    // 指定したパスを再生成
    await res.revalidate('/blog/first-post')
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).send('Error revalidating')
  }
}

このエンドポイントをCMSから呼び出すことで、コンテンツ公開時に即座にページを再生成できます:

curl -X POST 'https://yourdomain.com/api/revalidate?secret=MY_SECRET_TOKEN&path=/blog/first-post'

4. 実践的なコード例

ブログサイトの完全な例

// pages/blog/[slug].js
import { getBlogPosts, getBlogPostBySlug } from '../../lib/blog'
import Head from 'next/head'

export async function getStaticProps({ params }) {
  const post = await getBlogPostBySlug(params.slug)
  
  if (!post) {
    return {
      notFound: true
    }
  }

  return {
    props: {
      post
    },
    // 1時間ごとに再生成
    revalidate: 3600
  }
}

export async function getStaticPaths() {
  const posts = await getBlogPosts()
  
  return {
    paths: posts.map(post => ({
      params: { slug: post.slug }
    })),
    fallback: 'blocking'
  }
}

function BlogPost({ post }) {
  return (
    <>
      
        {post.title}
        
      
      

{post.title}

{post.content}
) } export default BlogPost

APIレスポンスをキャッシュする例

// pages/products/[id].js
export async function getStaticProps({ params }) {
  // 外部APIからデータ取得
  const response = await fetch(`https://api.example.com/products/${params.id}`)
  const product = await response.json()

  if (!product) {
    return {
      notFound: true
    }
  }

  return {
    props: {
      product
    },
    // 30分ごとに再生成
    revalidate: 1800
  }
}

export async function getStaticPaths() {
  // 人気商品のIDのみ事前生成
  const topProducts = await fetch('https://api.example.com/top-products')
    .then(res => res.json())

  return {
    paths: topProducts.map(product => ({
      params: { id: product.id.toString() }
    })),
    fallback: 'blocking'
  }
}

function ProductPage({ product }) {
  return (
    

{product.name}

価格: ¥{product.price}

{product.description}

) } export default ProductPage

5. よくある間違いと対策

間違い1:revalidateを設定し忘れる

問題のコード:

export async function getStaticProps({ params }) {
  const data = await fetchData(params.id)
  
  return {
    props: { data }
    // revalidateが指定されていない!
  }
}

解決方法:

export async function getStaticProps({ params }) {
  const data = await fetchData(params.id)
  
  return {
    props: { data },
    revalidate: 3600  // 必ず設定する
  }
}

間違い2:On-Demand ISRでセキュリティトークンを省略

危険なコード:

// これはダメな例です
export default async function handler(req, res) {
  // セキュリティチェックなし
  await res.revalidate(req.query.path)
  return res.json({ revalidated: true })
}

安全な実装:

export default async function handler(req, res) {
  // 環境変数から秘密トークンを取得
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Unauthorized' })
  }

  if (!req.query.path) {
    return res.status(400).json({ message: 'Path is required' })
  }

  try {
    await res.revalidate(req.query.path)
    return res.json({ revalidated: true })
  } catch (err) {
    return res.status(500).json({ message: 'Error revalidating' })
  }
}

間違い3:getStaticPathsでfallbackを誤解する

問題の例:

// fallback: falseの場合、事前生成されていないページは404になる
export async function getStaticPaths() {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: false  // /pages/2 にアクセスすると404
  }
}

正しい使い分け:

export async function getStaticPaths() {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: 'blocking'  // オンデマンドで生成して返す
    // fallback: true を使うと、クライアント側でデータフェッチが必要になる
  }
}

間違い4:再検証の時間を短すぎる値に設定

revalidateを極端に小さい値(例:1秒)に設定すると、ほぼSSRと同じになり、パフォーマンス改善効果がなくなります。

// 非効率な設定
return {
  props: { data },
  revalidate: 1  // これではSSRとほぼ同じ
}

// 適切な設定
return {
  props: { data },
  revalidate: 60  // 最低でも数十秒以上が目安
}

間違い5:ISRがサポートされていない環境での使用

ISRはサーバーレス環境(Vercelなど)で最適化されています。セルフホスティングの場合は別途設定が必要です。

// next.config.js - セルフホスティング時の設定例
module.exports = {
  experimental: {
    isrMemoryCacheSize: 52 * 1000 * 1000, // 52MB
  },
}

6. ISRのベストプラクティス

revalidateの値の決定方法

コンテンツタイプ 推奨revalidate値 理由
ニュース・速報 10~60秒 常に最新情報が必要
ブログ記事 3600秒(1時間) 1日数回の更新で十分
ECサイト商品 600~3600秒 在庫や価格の定期更新
静的ページ(About等) 86400秒(1日) 更新が稀

On-Demand ISRの活用

スケジュール更新ではなく、イベントベースの更新が必要な場合はOn-Demand ISRを使用します:

  • CMSから記事を公開した時点で即座に再生成
  • 在庫が更新された時に商品ページを再生成
  • ユーザーが重要な変更を加えた直後に関連ページを再生成

フォールバック戦略

// fallback: 'blocking'の使用が推奨
export async function getStaticPaths() {
  const popularPages = await getPopularPages()
  
  return {
    // 事前生成:アクセス頻度の高いページ
    paths: popularPages,
    // その他のページはオンデマンド生成
    fallback: 'blocking'
  }
}

7. ISRの監視と最適化

再生成の成功を監視する

// pages/api/revalidate.js
export default async function handler(req, res) {
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' })
  }

  try {
    const startTime = Date.now()
    await res.revalidate(req.query.path)
    const duration = Date.now() - startTime
    
    // ログに記録
    console.log(`Revalidated ${req.query.path} in ${duration}ms`)
    
    return res.json({ 
      revalidated: true,
      duration
    })
  } catch (err) {
    console.error(`Revalidation failed for ${req.query.path}:`, err)
    return res.status(500).json({ message: 'Error revalidating' })
  }
}

まとめ

Next.jsのISR(Incremental Static Regeneration)は、静的生成の高速性とSSRの柔軟性を組み合わせた強力な機能です。

重要なポイント:

  • ISRの仕組み:事前に静的ページを生成し、指定時間ごとにバックグラウンドで再生成
  • revalidateの設定:コンテンツの更新頻度に応じて適切な値を設定することが重要
  • On-Demand ISR:イベントベースの更新に対応可能
  • セキュリティ:再生成エンドポイントは必ず認証を実装
  • 最適化:すべてのページを事前生成するのではなく、人気ページのみを事前生成し、その他はオンデマンド生成

ISRを正しく活用することで、高速でスケーラブル、かつ常に最新のコンテンツを提供するNext.jsアプリケーションを構築できます。プロジェクトの要件に応じて、revalidateの値やfallbackの設定を調整し、最適な戦略を選択することが成功の鍵です。

タイトルとURLをコピーしました