Menu

インクリメンタル静的再生成 (ISR)

インクリメンタル静的再生成(ISR)には以下のような利点があります:

  • サイト全体を再ビルドすることなく静的コンテンツを更新できます
  • ほとんどのリクエストに対して事前レンダリングされた静的ページを提供することでサーバーの負荷を軽減します
  • ページに適切なcache-controlヘッダーが自動的に追加されます
  • next buildの時間を長くすることなく、大量のコンテンツページを処理できます

以下は最小限の例です:

pages/blog/[id].tsx
TypeScript
import type { GetStaticPaths, GetStaticProps } from 'next'
 
interface Post {
  id: string
  title: string
  content: string
}
 
interface Props {
  post: Post
}
 
export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
  const paths = posts.map((post: Post) => ({
    params: { id: String(post.id) },
  }))
 
  // ビルド時にはこれらのパスのみを事前レンダリングします。
  // { fallback: 'blocking' }は、パスが存在しない場合に
  // オンデマンドでページをサーバーレンダリングします。
  return { paths, fallback: false }
}
 
export const getStaticProps: GetStaticProps<Props> = async ({
  params,
}: {
  params: { id: string }
}) => {
  const post = await fetch(`https://api.vercel.app/blog/${params.id}`).then(
    (res) => res.json()
  )
 
  return {
    props: { post },
    // Next.jsは60秒に1回まで、リクエストが来た際に
    // キャッシュを無効化します。
    revalidate: 60,
  }
}
 
export default function Page({ post }: Props) {
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  )
}

この例の動作は以下の通りです:

  1. next buildの間に、既知のすべてのブログ記事が生成されます(この例では25記事)
  2. これらのページへのすべてのリクエスト(例:/blog/1)はキャッシュされ、即座に返されます
  3. 60秒経過後も、次のリクエストはキャッシュされた(古い)ページを表示します
  4. キャッシュが無効化され、新しいバージョンのページがバックグラウンドで生成を開始します
  5. 正常に生成されると、Next.jsは更新されたページを表示してキャッシュします
  6. /blog/26がリクエストされた場合、Next.jsはこのページをオンデマンドで生成してキャッシュします

リファレンス

関数

res.revalidate()を使用したオンデマンド検証

より正確な再検証方法として、API Routerからres.revalidateを使用して、オンデマンドで新しいページを生成できます。

例えば、このAPI Routeは/api/revalidate?secret=<token>で呼び出され、指定されたブログ記事を再検証します。Next.jsアプリケーションのみが知る秘密トークンを作成してください。この秘密は再検証APIルートへの不正アクセスを防ぐために使用されます。

pages/api/revalidate.ts
import type { NextApiRequest, NextApiResponse } from 'next'
 
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // 有効なリクエストであることを確認するために秘密をチェック
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' })
  }
 
  try {
    // これは実際のパスである必要があり、書き換えられたパスではありません
    // 例:"/posts/[id]"の場合、これは"/posts/1"となります
    await res.revalidate('/posts/1')
    return res.json({ revalidated: true })
  } catch (err) {
    // エラーが発生した場合、Next.jsは
    // 最後に正常に生成されたページを表示し続けます
    return res.status(500).send('Error revalidating')
  }
}
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 {
    // これは実際のパスである必要があり、書き換えられたパスではありません
    // 例:"/posts/[id]"の場合、これは"/posts/1"となります
    await res.revalidate('/posts/1')
    return res.json({ revalidated: true })
  } catch (err) {
    // エラーが発生した場合、Next.jsは
    // 最後に正常に生成されたページを表示し続けます
    return res.status(500).send('Error revalidating')
  }
}

オンデマンド再検証を使用する場合、getStaticProps内でrevalidate時間を指定する必要はありません。Next.jsはデフォルト値のfalse(再検証なし)を使用し、res.revalidate()が呼び出されたときにのみページをオンデマンドで再検証します。

キャッチされない例外の処理

バックグラウンド再生成を処理する際にgetStaticProps内でエラーが発生した場合、または手動でエラーをスローした場合、最後に正常に生成されたページが引き続き表示されます。次の後続のリクエストで、Next.jsはgetStaticPropsの呼び出しを再試行します。

pages/blog/[id].tsx
TypeScript
import type { GetStaticProps } from 'next'
 
interface Post {
  id: string
  title: string
  content: string
}
 
interface Props {
  post: Post
}
 
export const getStaticProps: GetStaticProps<Props> = async ({
  params,
}: {
  params: { id: string }
}) => {
  // このリクエストでキャッチされないエラーがスローされた場合、
  // Next.jsは現在表示されているページを無効化せず、
  // 次のリクエストで getStaticProps を再試行します。
  const res = await fetch(`https://api.vercel.app/blog/${params.id}`)
  const post: Post = await res.json()
 
  if (!res.ok) {
    // サーバーエラーがある場合、
    // 次の成功したリクエストまでキャッシュが更新されないように、
    // 返却する代わりにエラーをスローすることをお勧めします。
    throw new Error(`Failed to fetch posts, received status ${res.status}`)
  }
 
  return {
    props: { post },
    // Next.jsは60秒に1回まで、リクエストが来た際に
    // キャッシュを無効化します。
    revalidate: 60,
  }
}

キャッシュの場所のカスタマイズ

ページのキャッシュと再検証(インクリメンタル静的再生成を使用)は、同じ共有キャッシュを使用します。Vercelにデプロイする場合、ISRキャッシュは自動的に永続ストレージに保存されます。

セルフホスティングの場合、ISRキャッシュはNext.jsサーバーのファイルシステム(ディスク)に保存されます。これはPagesとApp Routerの両方を使用してセルフホスティングする際に自動的に機能します。

キャッシュされたページとデータを永続ストレージに保存したい場合や、Next.jsアプリケーションの複数のコンテナまたはインスタンス間でキャッシュを共有したい場合は、Next.jsのキャッシュの場所を設定できます。詳細はこちらをご覧ください。

トラブルシューティング

ローカル開発でのキャッシュされたデータのデバッグ

fetch APIを使用している場合、どのリクエストがキャッシュされているか、またはキャッシュされていないかを理解するために追加のログを追加できます。loggingオプションの詳細はこちらをご覧ください。

next.config.js
module.exports = {
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
}

本番動作の確認

本番環境でページが正しくキャッシュされ、再検証されていることを確認するには、next buildを実行し、その後next startを実行して本番のNext.jsサーバーを実行することでローカルでテストできます。

これにより、本番環境と同様のISRの動作をテストすることができます。さらにデバッグを行うには、.envファイルに以下の環境変数を追加してください:

.env
NEXT_PRIVATE_DEBUG_CACHE=1

これにより、Next.jsサーバーはISRキャッシュのヒットとミスをコンソールログに出力します。この出力を確認することで、next build中に生成されたページや、パスがオンデマンドでアクセスされた際にページがどのように更新されるかを確認できます。

注意事項

  • ISRはNode.jsランタイム(デフォルト)を使用する場合のみサポートされています。
  • Static Exportを作成する場合、ISRはサポートされていません。
  • オンデマンドISRリクエストに対してミドルウェアは実行されません。つまり、ミドルウェアのパスの書き換えやロジックは適用されません。正確なパスを再検証していることを確認してください。例えば、書き換えられた/post-1ではなく、/post/1を使用してください。

バージョン履歴

バージョン変更内容
v14.1.0カスタムcacheHandlerが安定版に
v13.0.0App Routerが導入
v12.2.0Pages Router: オンデマンドISRが安定版に
v12.0.0Pages Router: ボットを意識したISRフォールバックが追加
v9.5.0Pages Router: 安定版ISRが導入