2025年06月22日7 分钟

ISR 增量静态再生:SSG 和 SSR 的完美中间地带

深入理解 ISR(Incremental Static Regeneration)如何在不重新构建的情况下更新静态页面

什么是 ISR

ISR(Incremental Static Regeneration,增量静态再生)是 Next.js 提出的渲染策略,它结合了 SSG 和 SSR 的优点:

  • 像 SSG 一样:页面是预渲染的静态 HTML,通过 CDN 快速分发
  • 像 SSR 一样:页面可以在后台定时更新,不需要完整重新构建
SSG:  构建时生成一次 → 永远不变(除非重新构建)
SSR:  每次请求都重新生成 → 永远最新(但服务器压力大)
ISR:  构建时生成 → 过期后在后台重新生成 → 兼顾速度和新鲜度

ISR 的工作原理

时间线:
  0s        构建完成,生成 HTML
  │
  ▼
  用户A访问 → 返回缓存的 HTML(即时)       ← 和 SSG 一样快
  用户B访问 → 返回缓存的 HTML(即时)
  │
  60s       revalidate 时间到期
  │
  ▼
  用户C访问 → 返回旧的缓存 HTML(即时)     ← 用户不用等
             → 同时后台触发重新生成          ← stale-while-revalidate
  │
  ▼
  后台生成完成 → 新 HTML 替换旧缓存
  │
  ▼
  用户D访问 → 返回新的 HTML(即时)          ← 内容已更新

核心策略:Stale-While-Revalidate

           请求到达
              │
              ▼
      ┌───── 缓存是否过期? ─────┐
      │                          │
     否                          是
      │                          │
      ▼                          ▼
  返回缓存                  返回旧缓存(先给用户)
  (即时)                  同时后台重新生成
                                 │
                                 ▼
                           生成完成 → 更新缓存
                                 │
                                 ▼
                           下次请求返回新内容

关键:过期后的第一个请求仍然返回旧内容(不让用户等),但触发后台更新。下一个请求才会拿到新内容。

Next.js 中的 ISR 实现

App Router(推荐)

// app/products/[id]/page.tsx

async function ProductPage({ params }: { params: { id: string } }) {
  // fetch 添加 revalidate 选项 = ISR
  const res = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 },  // 60 秒后允许重新生成
  })
  const product = await res.json()

  return (
    <div>
      <h1>{product.name}</h1>
      <p>价格:¥{product.price}</p>
      <p>库存:{product.stock}</p>
    </div>
  )
}

也可以在页面级别设置:

// app/products/[id]/page.tsx

// 整个页面每 60 秒重新验证一次
export const revalidate = 60

async function ProductPage({ params }) {
  const product = await getProduct(params.id)
  return <div>{product.name}</div>
}

Pages Router(旧版)

// pages/products/[id].tsx

export async function getStaticProps({ params }) {
  const product = await getProduct(params.id)
  return {
    props: { product },
    revalidate: 60,  // ISR:60 秒后重新生成
  }
}

export async function getStaticPaths() {
  const popular = await getPopularProducts()
  return {
    paths: popular.map(p => ({ params: { id: p.id } })),
    fallback: 'blocking',  // 未预渲染的页面:首次访问时 SSR,之后缓存
  }
}

按需重新验证(On-Demand Revalidation)

除了定时过期,Next.js 还支持主动触发重新生成:

// app/api/revalidate/route.ts

import { revalidatePath, revalidateTag } from 'next/cache'

export async function POST(req: Request) {
  const { secret, path, tag } = await req.json()

  // 验证密钥
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 })
  }

  // 方式1:按路径重新生成
  if (path) {
    revalidatePath(path)  // 如 '/products/123'
  }

  // 方式2:按标签重新生成
  if (tag) {
    revalidateTag(tag)  // 如 'products'
  }

  return Response.json({ revalidated: true })
}

实际应用:CMS Webhook

编辑在 CMS 中更新了商品价格
         │
         ▼
CMS 发送 Webhook → POST /api/revalidate
                   { secret: "xxx", path: "/products/123" }
         │
         ▼
Next.js 后台重新生成 /products/123 的 HTML
         │
         ▼
下次用户访问 → 看到最新价格
         │
整个过程: 编辑更新 → 几秒内生效 → 不需要完整重新构建

ISR 的三种 fallback 策略

对于构建时未预渲染的页面(如新增的商品),有三种处理方式:

export async function getStaticPaths() {
  return {
    paths: [...],

    // 方式1:fallback: false
    // 未预渲染的路径 → 直接返回 404
    // 适用:页面数量固定,不会新增

    // 方式2:fallback: true
    // 未预渲染的路径 → 先返回 fallback UI(Loading)
    // → 后台生成 → 客户端更新为真实内容
    // 适用:大量页面,首次访问可以等一下

    // 方式3:fallback: 'blocking'
    // 未预渲染的路径 → 服务端阻塞生成(像 SSR)
    // → 生成完成后返回完整 HTML → 之后缓存
    // 适用:不想显示 Loading,SEO 重要

    fallback: 'blocking',
  }
}
fallback: false
  已预渲染页面 → 返回 HTML
  未预渲染页面 → 404

fallback: true
  已预渲染页面 → 返回 HTML
  未预渲染页面 → 返回 Loading → 后台生成 → 客户端更新

fallback: 'blocking'
  已预渲染页面 → 返回 HTML
  未预渲染页面 → 等待生成完成 → 返回完整 HTML

四种渲染模式对比总结

特性CSRSSRSSGISR
渲染时机浏览器端每次请求时构建时构建时 + 定时更新
首屏速度最快最快
SEO
数据新鲜度实时实时构建时近实时
服务器负担
CDN 缓存
个性化内容
适用场景后台系统动态页面静态内容大多数场景

选择决策树

你的页面需要……
  │
  ├─ 完全不需要 SEO + 强交互 → CSR
  │
  ├─ 需要 SEO
  │   ├─ 数据很少变化 → SSG
  │   ├─ 数据偶尔变化 → ISR(推荐)
  │   └─ 数据实时变化 + 个性化 → SSR
  │
  └─ 同一页面混合需求
      └─ 静态壳 SSG/ISR + 动态部分 CSR(推荐组合)

总结

ISR 是目前 Next.js 推荐的默认渲染策略,它用极低的成本获得了接近 SSG 的性能和接近 SSR 的数据新鲜度。配合按需重新验证(On-Demand Revalidation),可以实现"内容更新后秒级生效"的效果,是大多数内容型网站的最佳选择。