SSR/SSG/ISR 渲染策略
Next.js 16 提供四种渲染策略:SSR(每次请求渲染)、SSG(构建时渲染)、ISR(定时刷新)、Streaming SSR(流式渲染)。不同的页面适用不同的策略——AI 对话页需要 SSR,博客文章适合 SSG,定价页适合 ISR。本章用实际场景讲清每种策略的原理、配置和选择依据。
1. 渲染策略全景
1.1 四种策略对比
| 策略 | 渲染时机 | 缓存 | TTFB | 适用场景 |
|---|---|---|---|---|
| SSG | 构建时 | 永久 | 极快 | 博客、文档、Marketing |
| ISR | 构建时 + 定时刷新 | 有限时间 | 快 | 定价页、产品列表 |
| SSR | 每次请求 | 无 | 较慢 | 用户仪表盘、AI 对话 |
| Streaming SSR | 每次请求 + 流式 | 无 | 快(渐进) | AI 对话、复杂仪表盘 |
1.2 Next.js 16 的自动判断
App Router 会自动判断页面应该用哪种策略——根据你的代码行为:
// 自动 SSG — 没有动态 API 调用
export default function PricingPage() {
return <div>$29/月</div>
}
// 自动 SSR — 使用了动态 API
export default async function ChatPage() {
const session = await auth() // 读取 cookies → 动态
const chats = await getChats(session.userId)
return <ChatList chats={chats} />
}触发动态渲染的 API:
cookies()/headers()— 读取请求信息searchParams— 读取查询参数fetch不带缓存选项connection()/draftMode()
2. SSG 静态站点生成
2.1 基础 SSG
不使用任何动态 API 的页面自动变为 SSG:
// app/blog/[slug]/page.tsx — 构建时生成所有文章页面
// 告诉 Next.js 有哪些 slug
export async function generateStaticParams() {
const posts = await db.query.posts.findMany({ columns: { slug: true } })
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound()
return (
<article className="prose mx-auto">
<h1>{post.title}</h1>
<time>{post.publishedAt.toLocaleDateString()}</time>
<MDXContent source={post.content} />
</article>
)
}构建输出:
.next/server/app/blog/
├── hello-world.html # 静态 HTML
├── nextjs-16-features.html
└── ai-saas-guide.html
2.2 dynamicParams 控制
// 构建时未生成的 slug 会怎样?
// 默认:动态生成(第一次请求时 SSR,然后缓存)
export const dynamicParams = true
// 严格模式:未生成的 slug 返回 404
export const dynamicParams = false2.3 适用场景
SSG 适合:
├── 博客文章 /blog/[slug]
├── 文档 /docs/[...slug]
├── Marketing 页面 /pricing, /about
├── Changelog /changelog
└── 静态落地页
3. ISR 增量静态再生
3.1 基于时间的 ISR
// app/pricing/page.tsx
// 页面每 3600 秒(1 小时)重新生成
export const revalidate = 3600
export default async function PricingPage() {
const plans = await db.query.plans.findMany()
return (
<div className="grid grid-cols-3 gap-8">
{plans.map((plan) => (
<PricingCard key={plan.id} plan={plan} />
))}
</div>
)
}ISR 的工作流程:
T=0 构建时生成静态页面
T=1 用户 A 访问 → 返回缓存的静态页面(极快)
T=100 用户 B 访问 → 返回缓存的静态页面(极快)
T=3600 缓存过期
T=3601 用户 C 访问 → 返回旧缓存 + 后台触发重新生成
T=3602 重新生成完成 → 替换缓存
T=3603 用户 D 访问 → 返回新生成的页面
关键:ISR 是 stale-while-revalidate 模式——用户永远不会等待,总是先返回缓存。
3.2 按需重新验证
除了定时,还可以通过 API 手动触发重新验证:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const { path, tag, secret } = await request.json()
// 验证密钥
if (secret !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid secret' }, { status: 401 })
}
if (path) {
revalidatePath(path) // 重新验证指定路径
}
if (tag) {
revalidateTag(tag) // 重新验证指定标签的所有缓存
}
return NextResponse.json({ revalidated: true })
}CMS 发布内容后调用:
curl -X POST https://your-app.com/api/revalidate \
-H "Content-Type: application/json" \
-d '{"path": "/blog/new-post", "secret": "your-secret"}'4. SSR 动态渲染
4.1 自动 SSR
使用动态 API 的页面自动变为 SSR:
// app/(app)/chat/page.tsx — 每次请求都重新渲染
export default async function ChatPage({ searchParams }: {
searchParams: Promise<{ model?: string }>
}) {
const session = await auth() // cookies() → 自动 SSR
const { model } = await searchParams
const chats = await db.query.chats.findMany({
where: and(
eq(chats.userId, session.user.id),
model ? eq(chats.model, model) : undefined,
),
orderBy: desc(chats.updatedAt),
})
return <ChatList chats={chats} />
}4.2 强制 SSR
// 即使没有动态 API,也强制每次请求渲染
export const dynamic = 'force-dynamic'
export default async function AnalyticsPage() {
const stats = await getRealtimeStats()
return <Dashboard stats={stats} />
}4.3 SSR 的性能问题
SSR 的问题:数据获取阶段用户看到的是白屏(等待所有数据准备好才发送 HTML)。
解决方案 → Streaming SSR。
5. Streaming SSR
5.1 原理
Streaming SSR 将页面分成多个独立的块,每个块准备好就立即发送到浏览器:
传统 SSR:
[等待所有数据] ────────────────→ [发送完整 HTML]
│
▼
用户看到页面
Streaming SSR:
[发送 HTML 外壳 + loading] ──→ 用户立即看到骨架屏
[数据块 1 准备好] ───────────→ 替换 loading 1
[数据块 2 准备好] ───────────→ 替换 loading 2
[数据块 3 准备好] ───────────→ 替换 loading 3
5.2 使用 loading.tsx 实现
// app/(dashboard)/chat/loading.tsx
export default function ChatLoading() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
</div>
)
}Next.js 自动将 loading.tsx 包裹在 <Suspense> 中:
// 等价于:
<Suspense fallback={<ChatLoading />}>
<ChatPage />
</Suspense>5.3 手动 Suspense 实现细粒度流式
// app/(dashboard)/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
{/* 三个区域独立流式加载 */}
<Suspense fallback={<StatsSkeleton />}>
<UsageStats /> {/* 独立异步数据获取 */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<TokenChart /> {/* 独立异步数据获取 */}
</Suspense>
<Suspense fallback={<ListSkeleton />}>
<RecentChats /> {/* 独立异步数据获取 */}
</Suspense>
</div>
)
}
// 每个组件独立获取数据
async function UsageStats() {
const stats = await getUsageStats() // 可能需要 200ms
return <StatsCard stats={stats} />
}
async function TokenChart() {
const data = await getTokenUsage() // 可能需要 500ms
return <Chart data={data} />
}
async function RecentChats() {
const chats = await getRecentChats() // 可能需要 100ms
return <ChatList chats={chats} />
}用户体验:
T=0ms 用户看到三个骨架屏
T=100ms RecentChats 加载完成 → 替换第三个骨架屏
T=200ms UsageStats 加载完成 → 替换第一个骨架屏
T=500ms TokenChart 加载完成 → 替换第二个骨架屏
5.4 Partial Prerendering (PPR)
Next.js 16 的实验性特性——将 SSG 和 Streaming SSR 结合:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
ppr: true,
},
}// app/(dashboard)/page.tsx
// PPR:静态部分构建时生成,动态部分流式加载
export default function DashboardPage() {
return (
<div>
{/* 静态部分 — 构建时生成,缓存在 CDN */}
<h1>仪表盘</h1>
<StaticNavigation />
{/* 动态部分 — 每次请求流式加载 */}
<Suspense fallback={<StatsSkeleton />}>
<DynamicStats />
</Suspense>
</div>
)
}PPR 的效果:
T=0ms CDN 立即返回静态 HTML(标题 + 导航 + 骨架屏)— TTFB 极快
T=50ms 用户看到页面框架
T=200ms 动态数据流式到达 → 替换骨架屏
6. 渲染策略选择指南
6.1 AI SaaS 页面策略映射
| 页面 | 策略 | 原因 | 配置 |
|---|---|---|---|
首页 / | SSG | 内容固定 | 默认 |
定价 /pricing | ISR 1h | 偶尔更新 | revalidate = 3600 |
博客 /blog/[slug] | SSG | 内容固定 | generateStaticParams |
文档 /docs | SSG | 构建时生成 | generateStaticParams |
对话列表 /chat | Streaming SSR | 用户数据+实时 | <Suspense> |
对话详情 /chat/[id] | Streaming SSR | 用户数据+实时 | <Suspense> |
设置 /settings | SSR | 用户数据 | 自动 |
管理后台 /admin | SSR | 敏感数据 | force-dynamic |
6.2 决策树
页面内容是否因用户不同而不同?
├── 否 → 内容多久更新一次?
│ ├── 几乎不更新 → SSG
│ ├── 每小时/每天 → ISR (revalidate)
│ └── 实时更新 → SSR
└── 是 → 页面有多个独立数据区域?
├── 是 → Streaming SSR (Suspense)
└── 否 → SSR
本章小结
- SSG:构建时生成,CDN 缓存,TTFB 最快——适合博客、文档、Marketing
- ISR:SSG + 定时/按需刷新——适合定价页、产品列表
- SSR:每次请求渲染——适合用户仪表盘、设置页
- Streaming SSR:SSR + 流式 + Suspense——适合复杂仪表盘、AI 对话
- PPR:SSG + Streaming 的混合体——静态壳 + 动态内容,TTFB 和新鲜度兼顾
- 自动判断:Next.js 根据你使用的 API 自动选择策略,大多数情况不需要手动配置
下一章讲 Server Actions 全栈数据变更。