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 = false

2.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 包裹在 &lt;Suspense&gt; 中:

// 等价于:
<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内容固定默认
定价 /pricingISR 1h偶尔更新revalidate = 3600
博客 /blog/[slug]SSG内容固定generateStaticParams
文档 /docsSSG构建时生成generateStaticParams
对话列表 /chatStreaming SSR用户数据+实时&lt;Suspense&gt;
对话详情 /chat/[id]Streaming SSR用户数据+实时&lt;Suspense&gt;
设置 /settingsSSR用户数据自动
管理后台 /adminSSR敏感数据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 全栈数据变更。