Menu

Incremental Static Regeneration(ISR)の実装方法

Incremental Static Regeneration(ISR)により、以下が実現できます:

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

最小限の例を以下に示します:

app/blog/[id]/page.tsx
TypeScript
interface Post {
  id: string
  title: string
  content: string
}
 
// Next.jsは最大60秒ごとに1回、
// リクエストが来たときにキャッシュを無効化します。
export const revalidate = 60
 
export async function generateStaticParams() {
  const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
  return posts.map((post) => ({
    id: String(post.id),
  }))
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
    (res) => res.json()
  )
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  )
}

この例がどのように機能するかは以下の通りです:

  1. next build中に、すべての既知のブログ記事が生成されます
  2. これらのページへのすべてのリクエスト(例:/blog/1)がキャッシュされ、即座に返されます
  3. 60秒経過後、次のリクエストはキャッシュされた(古い)ページを返します
  4. キャッシュが無効化され、ページの新しいバージョンがバックグラウンドで生成され始めます
  5. 正常に生成されたら、次のリクエストは更新されたページを受け取り、後続のリクエスト用にキャッシュされます
  6. /blog/26がリクエストされ、それが存在する場合、ページはオンデマンドで生成されます。この動作は別のdynamicParams値を使用することで変更できます。ただし、記事が存在しない場合は404が返されます。

リファレンス

Route segment config

関数

時間ベースの再検証

これは/blogのブログ記事のリストを取得して表示します。1時間経過後、次の訪問者はすぐに高速な応答を得るためにキャッシュされた(古い)バージョンのページを受け取ります。同時に、Next.jsはバックグラウンドで新しいバージョンの再生成をトリガーします。新しいバージョンが正常に生成されたら、キャッシュされたバージョンを置き換え、後続の訪問者は更新されたコンテンツを受け取ります。

app/blog/page.tsx
TypeScript
interface Post {
  id: string
  title: string
  content: string
}
 
export const revalidate = 3600 // 1時間ごとに無効化
 
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog')
  const posts: Post[] = await data.json()
  return (
    <main>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  )
}

高い再検証時間を設定することをお勧めします。例えば、1秒ではなく1時間にします。より高い精度が必要な場合は、オンデマンド再検証の使用を検討してください。リアルタイムデータが必要な場合は、動的レンダリングへの切り替えを検討してください。

revalidatePathを使用したオンデマンド再検証

より正確な再検証方法のために、revalidatePath関数を使用してキャッシュされたページをオンデマンドで無効化します。

例えば、このServer Actionは新しい記事を追加した後に呼び出されます。fetchを使用してデータを取得するか、データベースに接続するかに関わらず、ルート全体のキャッシュを無効化します。その後のそのルートへのリクエストは再生成をトリガーし、新鮮なデータを提供します。新鮮なデータはその後の後続リクエストのためにキャッシュされます。

補足: revalidatePathはキャッシュエントリを無効化しますが、再生成は次のリクエストで発生します。次のリクエストを待つ代わりに、すぐにキャッシュエントリを積極的に再生成したい場合は、Pages routerのres.revalidateメソッドを使用できます。App Routerに積極的な再生成機能を提供するための新しいメソッドの追加に取り組んでいます。

app/actions.ts
TypeScript
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  // /postsルートのキャッシュを無効化
  revalidatePath('/posts')
}

デモを表示して、ソースコードを確認してください。

revalidateTagを使用したオンデマンド再検証

ほとんどのユースケースでは、パス全体の再検証が推奨されます。より細粒度な制御が必要な場合は、revalidateTag関数を使用できます。例えば、個別のfetch呼び出しにタグを付けることができます:

app/blog/page.tsx
TypeScript
export default async function Page() {
  const data = await fetch('https://api.vercel.app/blog', {
    next: { tags: ['posts'] },
  })
  const posts = await data.json()
  // ...
}

ORMを使用しているか、データベースに接続している場合は、unstable_cacheを使用できます:

app/blog/page.tsx
TypeScript
import { unstable_cache } from 'next/cache'
import { db, posts } from '@/lib/db'
 
const getCachedPosts = unstable_cache(
  async () => {
    return await db.select().from(posts)
  },
  ['posts'],
  { revalidate: 3600, tags: ['posts'] }
)
 
export default async function Page() {
  const posts = getCachedPosts()
  // ...
}

その後、Server ActionsまたはRoute HandlerrevalidateTagを使用できます:

app/actions.ts
TypeScript
'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  // 「posts」タグ付きのすべてのデータを無効化
  revalidateTag('posts')
}

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

データを再検証する際にエラーがスローされた場合、最後に正常に生成されたデータはキャッシュから提供され続けます。その後のリクエストで、Next.jsはデータの再検証を再試行します。エラーハンドリングの詳細を学習してください。

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

キャッシュされたページとデータを永続的なストレージに保持したい場合、または複数のコンテナーまたは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ランタイム(デフォルト)を使用する場合のみサポートされます。
  • ISRはStatic Exportを作成する場合はサポートされません。
  • 静的にレンダリングされたルートに複数のfetchリクエストがあり、それぞれが異なるrevalidate頻度を持つ場合、最も低い時間がISRに使用されます。ただし、これらの再検証頻度は引き続きData Cacheによって尊重されます。
  • ルートで使用されるfetchリクエストのいずれかにrevalidate時間が0の場合、または明示的なno-storeがある場合、ルートは動的にレンダリングされます。
  • プロキシはオンデマンドISRリクエストに対して実行されません。つまり、プロキシ内のパス書き直しまたはロジックは適用されません。正確なパスを再検証していることを確認します。例えば、書き直された/post-1ではなく/post/1です。

プラットフォームサポート

デプロイメントオプションサポート状況
Node.jsサーバーあり
Dockerコンテナあり
Static exportなし
Adaptersプラットフォーム依存

Next.jsを自己ホストする場合のISRの設定方法を学習してください。

バージョン履歴

バージョン変更内容
v14.1.0カスタムcacheHandlerが安定化。
v13.0.0App Routerが導入。
v12.2.0Pages Router:On-Demand ISRが安定化。
v12.0.0Pages Router:Bot-aware ISR fallbackを追加。
v9.5.0Pages Router:Stable ISRが導入