Next.js 渲染策略:SSG、SSR、ISR、CSR 的全景图

系统讲解 Next.js 支持的所有渲染策略:SSG/SSR/ISR/CSR 的定义、适用场景、Next.js 如何决定每个页面的渲染策略,以及 revalidate 和流式渲染与 React Suspense 的结合。

为什么理解渲染策略至关重要

选择错误的渲染策略是 Next.js 应用最常见的性能问题来源。在面试中,能否清晰解释这四种渲染模式的差异、trade-offs,以及在不同场景下的选择依据,是区分「会用框架」和「理解原理」的关键。

渲染策略直接影响三个核心指标:首屏时间(FCP/LCP)TTI(可交互时间)、以及服务端资源消耗。理解这些策略不是为了记住规则,而是为了能够在具体业务场景下做出工程判断。


一、四种渲染策略的定义

1.1 SSG(Static Site Generation,静态站点生成)

SSG 在构建时(build time) 生成页面的 HTML。每个 URL 对应一个预先生成好的 HTML 文件。用户访问时,服务端直接返回这个静态文件,不需要任何服务端计算。

构建时:
  posts/  →  生成 posts/index.html, posts/first-post.html 等

用户请求:
  GET /posts/first-post  →  返回已生成的 HTML(CDN 可缓存)

核心特征

  • 构建一次,反复使用
  • 极快的 TTFB(Time to First Byte)
  • 天然适合 CDN 边缘节点缓存
  • 适合内容不频繁变化的页面

适用场景

  • 博客文章、文档页面
  • 产品营销页面
  • 不需要个性化内容的页面
// app/posts/[slug]/page.tsx
// 默认静态生成(在 App Router 中)
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export default async function PostPage({ params }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}

1.2 SSR(Server-Side Rendering,服务端渲染)

SSR 在每次请求时(per-request) 动态生成 HTML。用户访问页面时,服务端执行页面组件,获取数据,渲染 HTML,然后返回。

用户请求:
  GET /dashboard  →  服务端执行 React 组件树(可能需要数据库查询)
                  →  生成 HTML
                  →  返回给浏览器

每次请求都执行完整的服务端计算

核心特征

  • 每次请求都有服务端计算成本
  • 数据始终新鲜(always fresh)
  • 可以访问请求上下文(cookies、headers)
  • 可以做个性化渲染

适用场景

  • 需要用户认证的页面(个性化内容)
  • 频繁变化的数据(股票行情、实时数据)
  • 需要 SEO 且内容个性化的页面
// app/dashboard/page.tsx
// 强制每次请求都服务端渲染
export const dynamic = 'force-dynamic';

export default async function DashboardPage() {
  const user = await getUser(); // 基于 cookie 获取当前用户
  const data = await fetchFreshData();

  return <Dashboard user={user} data={data} />;
}

1.3 ISR(Incremental Static Regeneration,增量静态再生)

ISR 是 Next.js 最独特的能力:页面静态生成,但可以在运行时(at runtime)重新验证(revalidate)和更新。这打破了"静态 = 构建时固定"的等式。

构建时:生成 HTML
运行时:定期重新验证,更新 HTML

用户请求:
  GET /products/shoes  →  返回缓存的 HTML
                      →  同时触发后台重新验证
                      →  下次请求时,缓存已更新

核心特征

  • 静态的性能(CDN 缓存、fast TTFB)
  • 动态的内容(定期更新)
  • 按需重新验证(on-demand revalidation)
// app/products/[id]/page.tsx
// 每 60 秒重新验证一次
export const revalidate = 60;

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  // 这个数据会在后台定期重新获取
  return <ProductDetail product={product} />;
}

1.4 CSR(Client-Side Rendering,客户端渲染)

CSR 完全在浏览器中渲染页面。服务端返回的 HTML 只是一个空壳(或者包含一个 loading 状态),真正的内容由 JavaScript 在浏览器中生成。

服务端返回:
  HTML 包含 <div id="root"></div> 和 bundle.js

浏览器执行:
  bundle.js → React 组件树 → DOM 操作 → 显示内容

核心特征

  • 首屏 HTML 几乎无内容(SEO 差)
  • 需要下载并执行 JS 才能看到内容
  • 适合高度交互的"应用"型页面

在 Next.js App Router 中的位置

App Router 中,页面默认是服务端组件,但页面内部可以包含客户端组件。真正的全 CSR 页面需要使用 dynamic 导入并禁用 SSR:

'use client';

// app/heavy-interactive/page.tsx
// 这个组件及其子树全部在客户端渲染
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  ssr: false, // 完全禁用服务端渲染
  loading: () => <ChartSkeleton />
});

export default function HeavyInteractivePage() {
  return <HeavyChart data={chartData} />;
}

二、Next.js 如何决定每个页面的渲染策略

Next.js 的渲染策略由多个配置点共同决定,理解它们的优先级顺序很重要。

2.1 决定树(Decision Tree)

页面有 generateStaticParams 吗?
  → 是:该页面 SSG(预渲染)
  → 否:进入动态渲染判断

页面有 dynamic = 'force-dynamic' 吗?
  → 是:SSR(每次请求渲染)

页面有 revalidate 设置吗?
  → 是:ISR(在后台定期重新验证)

以上都没有:
  → SSR(默认行为,除非是叶子节点且无动态数据?)
  → 实际上 App Router 默认是 SSG/ISR

2.2 核心配置项

配置 渲染策略
generateStaticParams 存在 SSG
dynamic = 'force-dynamic' 设置 SSR(每请求)
dynamic = 'force-static' 设置 SSG(强制静态)
revalidate = number 设置 ISR(定时重新验证)
默认(无配置) - SSG(自动静态优化)

2.3 fetch 请求的 cache 选项

在 App Router 中,fetch() 请求可以通过 cache 选项控制缓存行为:

// 默认:服务端组件的 fetch 会自动被缓存(等同于 SSG)
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache' // 默认值
});

// 每次请求重新获取(等同于 SSR)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// 定时重新验证(等同于 ISR)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
});

对于非 fetch 的数据获取(直接数据库查询等),使用 unstable_cache 或框架提供的数据缓存 API。


三、revalidate 和增量生成

3.1 时间基础重新验证(Time-based Revalidation)

// app/blog/page.tsx
// 每小时重新验证一次
export const revalidate = 3600;

async function getBlogPosts() {
  const posts = await fetch('https://cms.example.com/posts', {
    next: { revalidate: 3600 }
  });
  return posts.json();
}

3.2 按需重新验证(On-demand Revalidation)

当 CMS 更新了内容,需要立即重新验证缓存,而不是等待下一次定时重新验证:

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const secret = request.headers.get('x-revalidate-secret');

  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 });
  }

  if (body.type === 'post') {
    // 重新验证特定的路径
    revalidatePath(`/blog/${body.slug}`);
  }

  if (body.type === 'all-posts') {
    // 重新验证所有带有 'posts' 标签的缓存
    revalidateTag('posts');
  }

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

3.3 重新验证的传播机制

revalidatePath('/blog/my-post') 被调用时:

  1. Full Route Cache 中 /blog/my-post 的 HTML 被标记为过期
  2. Data Cache 中所有与这个路径关联的 fetch 请求被标记为过期
  3. 下一次请求到达时,Next.js 在后台重新渲染页面并缓存结果

四、流式渲染和 React Suspense 的结合

4.1 什么是流式渲染

传统 SSR 是"全有或全无":要么整个页面都渲染好了返回,要么就等待。但一个页面可能有多个数据来源,速度各不相同——快的部分等待慢的部分是不合理的。

流式渲染(Streaming)将页面拆分成多个Chunk,当一个 chunk 的数据准备好时就立即发送给浏览器,而不是等待所有数据都准备好。

传统 SSR:
  [========等待所有数据========]
  → 一次性返回完整 HTML

流式渲染:
  [Header(快)] → [Footer(快)] → [Content(慢)] → 逐步返回 HTML

4.2 Suspense:React 的流式原语

React 的 Suspense 是实现流式渲染的机制。<Suspense fallback={<Loading />}> 包裹一个异步组件,当异步数据还在加载时显示 fallback UI,数据就绪后自动切换到实际内容。

import { Suspense } from 'react';

// 慢的组件:数据获取可能耗时
async function UserProfile({ userId }) {
  const user = await fetchUser(userId); // 可能很慢
  return <Profile user={user} />;
}

async function UserActivity({ userId }) {
  const activity = await fetchActivity(userId); // 另一个慢的来源
  return <ActivityFeed activity={activity} />;
}

// loading 组件
function ProfileSkeleton() {
  return <div className="skeleton-profile">加载中...</div>;
}

// 主页面
export default function UserPage({ params }) {
  return (
    <div>
      <Header />

      {/* 这两个 Suspense 边界独立运作,互不阻塞 */}
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile userId={params.id} />
      </Suspense>

      <Suspense fallback={<ActivitySkeleton />}>
        <UserActivity userId={params.id} />
      </Suspense>

      <Footer />
    </div>
  );
}

在这个例子中,UserProfileUserActivity 是并行获取数据的。如果 UserProfile 先完成,就先显示;不需要等 UserActivity 也完成。

4.3 loading.tsx:Next.js 的 Suspense 集成

loading.tsx 是 Next.js 对 Suspense 的声明式封装。Next.js 会自动把页面内容包裹在 Suspense 边界中:

// app/posts/loading.tsx
// 这个 UI 会在 posts 页面数据加载时显示
export default function PostsLoading() {
  return (
    <div className="posts-grid">
      {Array(6).fill(null).map((_, i) => (
        <PostSkeleton key={i} />
      ))}
    </div>
  );
}

// app/posts/page.tsx
// Next.js 自动给这个组件加上 Suspense 边界
export default async function PostsPage() {
  const posts = await getPosts();
  return <PostsGrid posts={posts} />;
}

4.4 流式渲染的 HTTP 基础

流式渲染依赖 HTTP 的 Transfer-Encoding: chunked。当浏览器开始收到 chunked 响应时,会立即解析并渲染已收到的部分,而不是等待整个响应完成。这就是为什么用户会看到页面内容"逐步出现"而不是"一次性弹出"。

Next.js 在服务rito 渲染时自动使用 chunked 编码,但需要确保你的部署环境(Vercel、Node.js server 等)支持流式响应。


五、渲染策略的选择框架

选择渲染策略不是靠死记硬背,而是基于几个关键问题的回答:

问题 SSG ISR SSR CSR
内容多久变化一次? 很少(天/周级别) 频繁(小时/分钟级别) 每请求都变 取决于用户交互
需要个性化内容? 有限
SEO 重要吗? 非常重要 重要 重要 不重要
性能要求(FCP/LCP)? 极高 中等
服务端资源成本? 低(构建时计算) 高(每次请求)

实际场景选择

博客文章列表:SSG(构建时生成,不变化) 电商产品详情:ISR(价格库存可能变化,但不需要秒级更新) ** Twitter 时间线**:SSR + CSR(个性化+实时,但首屏需要 SEO) ** Google 表格应用**:CSR(完全用户交互驱动,无 SEO 需求)


六、面试高频问题

Q1: SSG 和 ISR 的区别是什么?

SSG 在构建时一次性生成 HTML,之后不更新。ISR 在构建时生成 HTML,但设置了 revalidate 后会在运行时定期重新验证和更新内容。ISR 本质上是"可更新的 SSG"。

Q2: 什么时候应该用 force-dynamic

当页面内容在每次请求时都不同,必须实时计算时使用。例如:需要读取 cookie 判断用户身份、需要展示实时数据、需要基于请求参数的个性化页面。

Q3: 流式渲染和传统 SSR 的性能差异在哪里?

传统 SSR 必须等待所有数据就绪才能开始发送 HTML,用户看到的是"白屏等待→完整页面"。流式渲染让快的数据先呈现,用户看到的是"骨架屏→逐步填充"。对用户感知性能(Perceived Performance)有显著提升。

Q4: ISR 的 revalidate 是如何工作的?

revalidate 设置后,Next.js 会在后台定期重新验证缓存的 fresh 数据。当缓存过期时,下一个请求会触发重新渲染并更新缓存。On-demand revalidate(revalidatePath/revalidateTag)可以立即使缓存失效,常用于 CMS 更新时主动推送刷新。


延展阅读