SSR、SSG 与 ISR:三种渲染策略的选型指南
第8章:SSR、SSG 与 ISR:三种渲染策略的选型指南
App Router 的渲染策略不是配置出来的,而是由组件的行为自动推导出来的。理解这一推导逻辑,是理解缓存与渲染系统的钥匙。
本章核心问题:App Router 如何自动决定渲染策略?SSG、ISR、SSR 分别适合什么场景?按需重新验证如何工作?
读完本章你将理解:
- 渲染策略的自动推导信号(cookies()、headers()、no-store 等)
- revalidate、generateStaticParams、dynamicParams 的配合使用
- revalidatePath 和 revalidateTag 实现事件驱动的缓存失效
Level 1 · 你需要知道的(1-3年经验)
渲染策略是每次请求级别,而非页面级别
App Router 的路由段(route segment)在每次请求时,会根据以下信号自动决定渲染策略:
| 信号 | 推导结果 |
|---|---|
调用 cookies() 或 headers() |
动态渲染(SSR) |
使用 searchParams prop |
动态渲染(SSR) |
fetch 调用设置 no-store |
动态渲染(SSR) |
export const dynamic = 'force-dynamic' |
强制动态渲染 |
| 无以上信号,且数据可缓存 | 静态渲染(SSG/ISR) |
export const revalidate = N |
ISR(每 N 秒重新生成) |
Next.js 在构建时静态分析每个路由段,如果能确定其输出与请求无关,就会静态生成。否则会在运行时动态渲染。
force-dynamic 与 force-static:强制覆盖
有时你需要显式告诉 Next.js 使用哪种策略,而不是依赖自动推导:
// app/dashboard/page.tsx
// 强制每次请求都重新渲染,即使 Next.js 认为可以缓存
export const dynamic = 'force-dynamic'
import { cookies } from 'next/headers'
import { db } from '@/lib/db'
export default async function DashboardPage() {
const cookieStore = await cookies()
const userId = cookieStore.get('user_id')?.value
if (!userId) {
redirect('/login')
}
const stats = await db.userStats.findUnique({ where: { userId } })
return <Dashboard stats={stats} />
}
// app/about/page.tsx
// 强制静态生成,即使页面中有某些动态信号
export const dynamic = 'force-static'
export default function AboutPage() {
// 即使在这里调用了某些通常触发动态渲染的 API
// force-static 会让 Next.js 忽略这些信号
return <div>关于我们页面(纯静态)</div>
}
dynamic 的可选值:
'auto'(默认):自动推导'force-dynamic':强制 SSR'force-static':强制静态'error':如果组件尝试动态渲染则报错(用于确保页面一定是静态的)
Level 2 · 它是怎么运行的(3-5年经验)
ISR:增量静态再生成
ISR 是 SSG 的进化形态:页面在构建时静态生成,但会在指定时间间隔后自动重新生成。在 App Router 中,通过 revalidate 导出实现:
// app/blog/[slug]/page.tsx
// 每 3600 秒(1小时)重新验证一次缓存
export const revalidate = 3600
import { db } from '@/lib/db'
interface Props {
params: Promise<{ slug: string }>
}
export default async function BlogPost({ params }: Props) {
const { slug } = await params
const post = await db.post.findUnique({
where: { slug },
include: { author: true, tags: true },
})
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<p>作者:{post.author.name}</p>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
ISR 的工作原理("Stale While Revalidate"语义):
- 首次请求时,如果没有缓存,动态渲染并缓存结果
revalidate时间内的后续请求直接返回缓存(stale)内容——毫秒级响应revalidate时间过后,下一个请求触发后台重新生成- 重新生成完成前,继续返回旧缓存;完成后,新内容生效
对用户来说,ISR 几乎总是返回缓存内容,性能接近纯静态;但内容会定期更新,不会永远过期。
generateStaticParams:动态路由的静态生成
对于 [slug]、[id] 这样的动态路由,Next.js 需要知道在构建时生成哪些路径。generateStaticParams 承担这个职责:
// app/products/[id]/page.tsx
import { db } from '@/lib/db'
// 构建时执行,告诉 Next.js 需要生成哪些路径
export async function generateStaticParams() {
const products = await db.product.findMany({
select: { id: true },
where: { active: true },
})
return products.map(product => ({
id: product.id,
}))
}
// 每个已生成路径的 revalidate 时间
export const revalidate = 86400 // 24小时
interface Props {
params: Promise<{ id: string }>
}
export default async function ProductPage({ params }: Props) {
const { id } = await params
const product = await db.product.findUnique({ where: { id } })
if (!product) notFound()
return (
<div>
<h1>{product.name}</h1>
<p>价格:¥{product.price}</p>
<p>库存:{product.stock}</p>
</div>
)
}
对于 generateStaticParams 未覆盖的路径(如新上架商品),Next.js 默认会在首次请求时动态渲染并缓存(按需 ISR)。可以通过 dynamicParams 控制这一行为:
// 对于未预生成的路径,返回 404 而不是动态渲染
export const dynamicParams = false
Level 3 · 规范怎么定义的(资深)
按需重新验证:revalidatePath 和 revalidateTag
ISR 的时间间隔有时太慢(等不了一小时内容才更新)。按需重新验证允许在数据变更时立即使缓存失效:
// app/api/webhooks/cms/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
// 验证 webhook 签名(安全性)
const signature = request.headers.get('x-webhook-signature')
if (!verifySignature(signature, await request.text())) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { type, slug, tags } = body
if (type === 'post.updated') {
// 使特定路径的缓存失效,触发重新生成
revalidatePath(`/blog/${slug}`)
// 同时使博客列表页失效
revalidatePath('/blog')
}
if (type === 'product.updated') {
// 使所有带有 'products' 标签的缓存失效
revalidateTag('products')
}
return NextResponse.json({ revalidated: true })
}
要使 revalidateTag 生效,需要在 fetch 调用时声明 tag:
// lib/api.ts
export async function getProducts() {
const response = await fetch('https://api.example.com/products', {
next: {
tags: ['products'], // 标记这个 fetch 的缓存属于 'products' 标签
revalidate: 3600,
},
})
return response.json()
}
export async function getProduct(id: string) {
const response = await fetch(`https://api.example.com/products/${id}`, {
next: {
tags: ['products', `product-${id}`], // 可以有多个标签
},
})
return response.json()
}
当 CMS 触发 webhook 调用 revalidateTag('products') 时,所有使用了 products 标签的 fetch 缓存都会失效,相关页面在下次请求时重新生成。
渲染策略选型决策框架
不同业务场景应该使用不同策略。以下是实战中的选型指南:
电商场景
// 商品详情页 —— ISR,商品信息变化不频繁
// app/products/[id]/page.tsx
export const revalidate = 300 // 5分钟
// 商品库存/价格 —— SSR,实时性要求高
// app/products/[id]/stock/route.ts
export const dynamic = 'force-dynamic'
// 购物车 —— SSR,用户专属,不可缓存
// app/cart/page.tsx
export const dynamic = 'force-dynamic'
// + 从 cookie/session 读取购物车数据
// 营销首页 —— ISR,运营频繁更新
// app/(marketing)/page.tsx
export const revalidate = 60 // 1分钟,配合 CMS webhook 按需刷新
// 品类导航 —— SSG,极少变化
// app/(marketing)/categories/page.tsx
export const revalidate = false // 永不自动重新验证,只通过 webhook 触发
博客/内容站场景
// 文章页 —— SSG + 按需 ISR
// app/posts/[slug]/page.tsx
export const revalidate = false // 不自动过期
export async function generateStaticParams() {
// 构建时生成所有已发布文章
const posts = await getPosts({ published: true })
return posts.map(p => ({ slug: p.slug }))
}
// 当 CMS 发布/更新文章时,通过 webhook 调用 revalidatePath
// 文章列表 —— ISR,1小时刷新
// app/posts/page.tsx
export const revalidate = 3600
// 作者主页 —— SSG,几乎不变
// app/authors/[id]/page.tsx
export const revalidate = 86400
SaaS 应用场景
// 仪表盘 —— SSR,用户实时数据
export const dynamic = 'force-dynamic'
// 设置页 —— SSR,读取用户配置
export const dynamic = 'force-dynamic'
// 公开文档 —— SSG
export const revalidate = false
// API 使用统计 —— SSR(实时),或 ISR(允许5分钟延迟)
export const revalidate = 300
fetch 级别的缓存控制
除了路由段级别的配置,Next.js 允许在单个 fetch 调用上精细控制缓存:
// app/page.tsx
export default async function HomePage() {
// 强制不缓存(等同于 SSR 行为)
const liveData = await fetch('https://api.example.com/live', {
cache: 'no-store',
})
// 使用 ISR 缓存,60秒后重新验证
const semiStaticData = await fetch('https://api.example.com/config', {
next: { revalidate: 60 },
})
// 永久缓存,直到手动 revalidate(等同于 SSG 行为)
const staticData = await fetch('https://api.example.com/constants', {
cache: 'force-cache',
})
// ...
}
如果一个路由中有任何 cache: 'no-store' 的 fetch,整个路由会被推导为动态渲染(除非设置了 force-static)。
Level 4 · 边界与陷阱(所有人)
陷阱1:如果一个路由中有任何 cache: 'no-store' 的 fetch,整个路由会被推导为动态渲染——即使其他 fetch 设置了缓存。
陷阱2:revalidatePath 和 revalidateTag 必须在 revalidate 调用之后、redirect 调用之前执行——redirect 内部抛出特殊错误,如果放在 try-catch 中可能被意外捕获。
陷阱3:Next.js 15 将 fetch 默认行为从 force-cache 改为 no-store——升级后性能下降通常是这个原因。
小结
App Router 的渲染策略是"约定推导"而非"显式配置":通过 cookies()、headers() 等动态信号的使用,或通过 dynamic、revalidate 等显式导出来控制。SSG 适合内容不变的页面;ISR 适合内容周期性更新的页面;SSR 适合实时性要求高或用户专属的页面。按需重新验证(revalidatePath / revalidateTag)将 ISR 的内容新鲜度从"时间驱动"提升为"事件驱动",是大多数内容型网站的最佳选择。