Skip to content

Next.js 增量静态再生 (ISR) 🔄

🎯 概述

增量静态再生(Incremental Static Regeneration,ISR)允许你在构建后更新静态页面,无需重新构建整个网站,结合了静态生成和服务端渲染的优势。

📦 什么是 ISR?

ISR 工作原理

  1. 首次请求返回缓存的静态页面
  2. 后台触发页面重新生成
  3. 新页面生成后替换旧缓存
  4. 后续请求获得更新的页面

ISR 优势

  • ⚡ 静态页面的快速响应
  • 🔄 内容可以定期更新
  • 💰 降低服务器负载
  • 🎯 按需更新特定页面
  • 📈 支持大规模网站

🚀 基本用法

App Router ISR

tsx
// 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

tsx
// 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>
}

📝 重新验证策略

基于时间的重新验证

tsx
// 每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()
}

按需重新验证

tsx
// 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 })
  }
}

触发重新验证:

bash
# 重新验证特定路径
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"

🎨 缓存标签

使用标签

tsx
// 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>
}

重新验证标签

tsx
// 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 })
}

🔄 路径重新验证

重新验证特定路径

tsx
// 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 })
}

重新验证布局

tsx
import { revalidatePath } from 'next/cache'

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

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

📊 实战示例

博客系统

tsx
// 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 })
}

电商产品页面

tsx
// 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 })
}

新闻网站

tsx
// 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 })
}

📚 最佳实践

1. 选择合适的重新验证时间

tsx
// 静态内容 - 长时间
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. 使用标签组织缓存

tsx
// 按类型标记
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. 错误处理

tsx
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. 监控和日志

tsx
// 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 })
  }
}

🔗 相关资源


下一步:学习 Next.js 客户端渲染 (CSR)