Next.js Incremental Static Regeneration (ISR)

Overview

Incremental Static Regeneration (ISR) lets you update static pages after build time without rebuilding the entire site, combining the benefits of static generation and server-side rendering.

What is ISR?

How ISR Works

  1. The first request returns a cached static page
  2. Page regeneration is triggered in the background
  3. The new page replaces the old cache once generated
  4. Subsequent requests receive the updated page

Advantages of ISR

  • Fast response times of static pages
  • Content can be updated periodically
  • Reduced server load
  • On-demand updates for specific pages
  • Supports large-scale websites

Basic Usage

App Router ISR

// app/posts/[id]/page.tsx
async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { revalidate: 60 } // 每60秒重新验证
  })
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

Pages Router ISR

// pages/posts/[id].tsx
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.id)
  
  return {
    props: { post },
    revalidate: 60, // 每60秒重新生成
  }
}

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: 'blocking',
  }
}

export default function Post({ post }) {
  return <article>{post.title}</article>
}

Revalidation Strategies

Time-Based Revalidation

// 每10秒重新验证
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 10 }
  })
  return res.json()
}

// 每小时重新验证
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }
  })
  return res.json()
}

// 每天重新验证
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 86400 }
  })
  return res.json()
}

On-Demand Revalidation

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret')
  
  // 验证密钥
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ message: '无效的密钥' }, { status: 401 })
  }
  
  const path = request.nextUrl.searchParams.get('path')
  const tag = request.nextUrl.searchParams.get('tag')
  
  try {
    if (path) {
      revalidatePath(path)
      return NextResponse.json({ revalidated: true, path })
    }
    
    if (tag) {
      revalidateTag(tag)
      return NextResponse.json({ revalidated: true, tag })
    }
    
    return NextResponse.json({ message: '缺少参数' }, { status: 400 })
  } catch (error) {
    return NextResponse.json({ message: '重新验证失败' }, { status: 500 })
  }
}

Trigger revalidation:

# 重新验证特定路径
curl -X POST "http://localhost:3000/api/revalidate?secret=MY_SECRET&path=/posts/1"

# 重新验证带标签的内容
curl -X POST "http://localhost:3000/api/revalidate?secret=MY_SECRET&tag=posts"

Cache Tags

Using Tags

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { 
      revalidate: 3600,
      tags: ['posts'] // 添加标签
    }
  })
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  return <div>{/* ... */}</div>
}

Revalidating Tags

// app/api/posts/route.ts
import { revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const post = await request.json()
  
  // 创建新文章
  await createPost(post)
  
  // 重新验证所有带 'posts' 标签的页面
  revalidateTag('posts')
  
  return NextResponse.json({ success: true })
}

Path Revalidation

Revalidating Specific Paths

// app/api/posts/[id]/route.ts
import { revalidatePath } from 'next/cache'
import { NextResponse } from 'next/server'

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const data = await request.json()
  
  // 更新文章
  await updatePost(params.id, data)
  
  // 重新验证文章页面
  revalidatePath(`/posts/${params.id}`)
  
  // 也重新验证文章列表
  revalidatePath('/posts')
  
  return NextResponse.json({ success: true })
}

Revalidating Layouts

import { revalidatePath } from 'next/cache'

// 重新验证特定布局下的所有页面
revalidatePath('/blog', 'layout')

// 重新验证特定页面
revalidatePath('/blog/post-1', 'page')

Practical Examples

Blog System

// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { 
      revalidate: 3600, // 1小时
      tags: ['posts', `post-${slug}`]
    }
  })
  return res.json()
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

// app/api/posts/[slug]/route.ts
import { revalidateTag, revalidatePath } from 'next/cache'

export async function PUT(request, { params }) {
  const data = await request.json()
  
  // 更新文章
  await updatePost(params.slug, data)
  
  // 重新验证这篇文章
  revalidateTag(`post-${params.slug}`)
  
  // 重新验证文章列表
  revalidatePath('/blog')
  
  return Response.json({ success: true })
}

E-Commerce Product Pages

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    next: { 
      revalidate: 300, // 5分钟
      tags: ['products', `product-${id}`]
    }
  })
  return res.json()
}

async function getReviews(productId: string) {
  const res = await fetch(`https://api.example.com/products/${productId}/reviews`, {
    next: { 
      revalidate: 60, // 1分钟
      tags: [`reviews-${productId}`]
    }
  })
  return res.json()
}

export default async function Product({ params }) {
  const [product, reviews] = await Promise.all([
    getProduct(params.id),
    getReviews(params.id),
  ])
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.price}元</p>
      <div>
        {reviews.map(review => (
          <div key={review.id}>{review.comment}</div>
        ))}
      </div>
    </div>
  )
}

// app/api/reviews/route.ts
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const review = await request.json()
  
  // 创建评论
  await createReview(review)
  
  // 只重新验证该产品的评论
  revalidateTag(`reviews-${review.productId}`)
  
  return Response.json({ success: true })
}

News Website

// app/news/page.tsx
async function getNews() {
  const res = await fetch('https://api.example.com/news', {
    next: { 
      revalidate: 60, // 1分钟
      tags: ['news']
    }
  })
  return res.json()
}

export default async function News() {
  const articles = await getNews()
  
  return (
    <div>
      {articles.map(article => (
        <article key={article.id}>
          <h2>{article.title}</h2>
          <p>{article.summary}</p>
        </article>
      ))}
    </div>
  )
}

// Webhook 接收新文章通知
// app/api/webhooks/news/route.ts
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const { secret, article } = await request.json()
  
  if (secret !== process.env.WEBHOOK_SECRET) {
    return Response.json({ error: '未授权' }, { status: 401 })
  }
  
  // 重新验证新闻页面
  revalidateTag('news')
  
  return Response.json({ revalidated: true })
}

Best Practices

1. Choose the Right Revalidation Interval

// 静态内容 - 长时间
const staticContent = await fetch('url', {
  next: { revalidate: 86400 } // 24小时
})

// 半静态内容 - 中等时间
const semiStatic = await fetch('url', {
  next: { revalidate: 3600 } // 1小时
})

// 动态内容 - 短时间
const dynamic = await fetch('url', {
  next: { revalidate: 60 } // 1分钟
})

// 实时内容 - 不缓存
const realtime = await fetch('url', {
  cache: 'no-store'
})

2. Organize Cache with Tags

// 按类型标记
async function getPosts() {
  const res = await fetch('url', {
    next: { tags: ['posts', 'content'] }
  })
  return res.json()
}

// 按ID标记
async function getPost(id: string) {
  const res = await fetch(`url/${id}`, {
    next: { tags: ['posts', `post-${id}`] }
  })
  return res.json()
}

// 重新验证时可以选择性更新
revalidateTag('posts') // 更新所有文章
revalidateTag('post-123') // 只更新特定文章

3. Error Handling

async function getData() {
  try {
    const res = await fetch('https://api.example.com/data', {
      next: { revalidate: 60 }
    })
    
    if (!res.ok) {
      throw new Error('获取失败')
    }
    
    return res.json()
  } catch (error) {
    console.error('数据获取错误:', error)
    // 返回缓存的数据或默认值
    return getCachedData()
  }
}

4. Monitoring and Logging

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'

export async function POST(request: Request) {
  const { path } = await request.json()
  
  console.log(`[ISR] 重新验证路径: ${path}`)
  
  try {
    revalidatePath(path)
    console.log(`[ISR] 成功重新验证: ${path}`)
    return Response.json({ success: true })
  } catch (error) {
    console.error(`[ISR] 重新验证失败: ${path}`, error)
    return Response.json({ error: '重新验证失败' }, { status: 500 })
  }
}

Previous Chapter: Static Site Generation (SSG) | Next Chapter: Client-Side Rendering (CSR)