Skip to main content

渲染模式:CSR / SSR / SSG / ISR / RSC

面试题:CSR、SSR、SSG、ISR、RSC 五种渲染模式的本质区别是什么?Next.js 怎么选?

一、五种模式一句话总结

模式全称渲染时机适合场景
CSRClient-Side Rendering浏览器后台、登录后页面
SSRServer-Side Rendering请求时(服务端)个性化、SEO、实时数据
SSGStatic Site Generation构建时博客、文档、营销页
ISRIncremental Static Regeneration构建时 + 增量大型电商、商品列表
RSCReact Server Components服务端(不 hydrate)数据密集型 UI

二、CSR(Client-Side Rendering)

2.1 流程

浏览器 ──→ 服务器(返回空 HTML + JS bundle)

├─ 下载 JS
├─ 执行 JS
├─ React 在浏览器渲染
└─ 显示内容

2.2 特点

  • ✅ 服务器压力小
  • ✅ 切页流畅(已是 SPA)
  • 首屏慢(白屏时间长)
  • 不 SEO 友好(HTML 无内容)
  • ❌ JS 体积全下发

2.3 典型场景

后台管理系统、登录后的页面(SEO 无意义)。

三、SSR(Server-Side Rendering)

3.1 流程

浏览器 ──→ 服务器(执行 React 渲染成 HTML 字符串)
│ ↓
│ 返回完整 HTML

浏览器展示 HTML ──→ 下载 JS ──→ hydrate(绑定事件)

3.2 核心:Hydration

Hydration(注入):浏览器拿到 SSR 返回的 HTML 后,下载 JS,在浏览器端重建 React 组件树,对比服务端生成的 DOM,绑定事件 + 同步状态

// 简化示意
const html = renderToString(<App />); // 服务端
// html = "<div><h1>Hello</h1><button>Click</button></div>"

hydrateRoot(document.getElementById("root"), <App />); // 客户端
// 浏览器复用已有 DOM,只补上事件监听

面试题:为什么需要 Hydration? 单纯返回 HTML 字符串是没有交互能力的(按钮点了没反应)。Hydration 让服务端渲染的「静态 HTML」变成可交互的 React 应用。

3.3 SSR 的两大痛点

  1. TTFB 慢:每个请求都要服务端现渲染
  2. 必须全量 hydrate:组件 JS 全要下发到浏览器

即使组件没交互,hydrate 也得把 JS 传过去。

四、SSG(Static Site Generation)

4.1 流程

构建时 ──→ 执行 React 渲染 ──→ 生成静态 HTML ──→ CDN 部署

浏览器 ──→ CDN 拉静态 HTML ──→ hydrate

4.2 特点

  • 最快首屏(CDN 缓存)
  • ✅ SEO 友好
  • ❌ 数据时效差(构建时定)
  • ❌ 大量页面构建慢

五、ISR(Incremental Static Regeneration)

SSG 的升级版:构建时不全量生成,请求时按需增量生成

5.1 流程

构建时 ──→ 只生成热门页面
用户访问冷门页面 ──→ 服务器现渲染 ──→ 缓存到 CDN
第二次访问 ──→ 直接命中 CDN

5.2 Next.js 中的实现

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

// 1. 告诉 Next 哪些路径要预生成
export async function generateStaticParams() {
const products = await db.product.findMany({ take: 100 });
return products.map(p => ({ id: String(p.id) }));
}

// 2. 配置重新验证时间(秒)
export const revalidate = 60; // 60s 后过期,过期时后台重新生成

5.3 特点

  • ✅ 大型网站友好(百万级页面可 scale)
  • ✅ 始终返回缓存(用户体验稳)
  • ❌ 数据有最多 revalidate 秒的延迟

六、RSC(React Server Components)

2024+ 革命性变化:服务端组件不 hydrate,代码完全不下发到浏览器

6.1 与 SSR 的核心区别

维度SSRRSC
JS 下发全部组件 JS(要 hydrate)只有 Client Component 的 JS
数据获取服务端渲染 → HTML 字符串服务端渲染 → RSC Payload 特殊格式
可直连 DB需要 API 间接调用组件内直接 await DB
Hydration全量 hydrate只 hydrate 标记 "use client" 的部分
bundle 体积整个应用可能减少 30%~70%

6.2 流程

浏览器 ──→ 服务器
├─ Server Component 渲染成 RSC Payload
│ (类似 JSON 的特殊格式,含组件树 + 数据)
├─ Client Component 标记位置 + JS bundle
└─ 一起返回

浏览器 ──→ 解析 RSC Payload ──→ 渲染成 HTML
──→ 下载 Client Component JS ──→ 局部 hydrate

6.3 RSC Payload 长什么样?

{
"type": "div",
"props": { "children": [
{
"type": "h1",
"props": { "children": "文章列表" }
},
{
"type": "ArticleItem",
"props": { "title": "Next.js 教程", "author": { "name": "Tom" } }
}
]}
}

关键:数据已经序列化在 Payload 里,客户端不需要再请求 API。Client Component 看到的就是「喂到嘴边的数据」

6.4 一句话区分

SSR 把组件代码发给浏览器,浏览器再 hydrate;RSC 把组件渲染结果发给浏览器,浏览器只 hydrate 需要交互的部分。

七、Next.js 的混合渲染能力

面试题:Next.js 怎么同时用 SSG + SSR + ISR?

Next.js 15 通过 路由级配置,每个页面/路由可独立选择渲染模式:

// app/page.tsx  →  默认 SSG(无 dynamic 函数)
// app/dashboard/page.tsx
export const dynamic = "force-dynamic"; // 强制 SSR

// app/products/[id]/page.tsx
export const revalidate = 60; // ISR
export const dynamicParams = true; // 动态参数也走 ISR

// app/api/route.ts
// → 始终是服务端 API 路由

决策树

页面要不要 SEO?
├─ 不要 → CSR(甚至不用 Next.js,用 Vite)
└─ 要 → 页面数据时效要求?
├─ 实时 → SSR(force-dynamic)
├─ 小时级 → ISR(revalidate=3600)
├─ 天级 → ISR 或 SSG
└─ 永久不变 → SSG

八、流式 SSR(Streaming)

面试题:Suspense + Streaming 解决了什么问题?

8.1 传统 SSR 的问题

// 假设页面分三块:Header、Sidebar、UserList
export default async function Page() {
const userList = await fetch("/api/users").then(r => r.json()); // 慢!
return (
<html>
<body>
<Header /> {/* 快 */}
<Sidebar /> {/* 快 */}
<UserList data={userList} /> {/* 慢 */}
</body>
</html>
);
}

问题:必须等 UserList 数据就绪,整个 HTML 才能返回。用户连 Header 都看不到

8.2 Streaming 方案

import { Suspense } from "react";

export default function Page() {
return (
<html>
<body>
<Header />
<Sidebar />
<Suspense fallback={<Skeleton />}>
<UserList /> {/* 慢的包在 Suspense 里 */}
</Suspense>
</body>
</html>
);
}

// UserList 是 async server component
async function UserList() {
const data = await fetch("/api/users").then(r => r.json());
return <ul>{data.map(...)}</ul>;
}

效果

T=0     服务端开始渲染
T=10ms Header HTML 流式发出
T=20ms Sidebar HTML 流式发出
T=30ms 浏览器已能看到 Header + Sidebar(Skeleton 占位)
T=500ms UserList 数据回来,HTML 继续流式发出,浏览器 patch 进 DOM

本质:把同步等待变成「先把能给的都给了,慢的部分用 Suspense 兜底」。

8.3 底层 API:renderToPipeableStream

// Next.js 内部使用(不用手写)
import { renderToPipeableStream } from "react-dom/server";

const stream = renderToPipeableStream(<App />, {
onShellReady() {
// 外层 HTML 准备好时立刻 flush(header/sidebar 不等 UserList)
stream.pipe(res);
},
onAllReady() {
// 全部准备好后才 flush(旧 SSR 行为)
},
});

面试题:onShellReady vs onAllReady? onShellReady 是流式的关键,不等所有 Suspense 解决就先把外层 HTML 推给浏览器,慢的部分通过 stream 后续追加。

九、五种模式速查决策表

场景推荐原因
博客 / 文档 / 营销页SSG一次构建,全 CDN 加速
商品详情 / 列表(百万级)ISR首屏快 + 数据近实时
个人中心 / 实时数据SSR每次请求拿最新数据
后台 / 工具型 SPACSRSEO 无关,强调交互体验
数据密集型 UI / 大量列表RSC零 JS 下发 + 直连 DB

十、面试高频追问

Q1:为什么 SSG 比 SSR 快?

A:SSG 在构建时就把 HTML 生成好放在 CDN,请求时直接拉静态文件;SSR 每次请求都要在服务端现渲染,受服务器性能 + 数据库查询影响。

Q2:SSR 与 RSC 的根本区别?

A:SSR 把组件代码发给浏览器再 hydrate,RSC 只发渲染结果不 hydrate。SSR 解决「首屏白屏」,RSC 解决「首屏白屏 + bundle 过大」。

Q3:什么时候不该用 RSC?

A:需要用户交互(onClick、useState)的组件必须用 Client Component。RSC 是「无状态、无交互、能直连 DB」的组件。

Q4:Next.js 的 dynamic = "force-dynamic" 和默认区别?

A:默认会静态化(能 SSG 就 SSG);force-dynamic 强制每次请求都跑服务端。如果组件里用了 cookies() / headers() 这种动态 API,Next.js 会自动转成动态渲染(不需要显式声明)。

Q5:hydrate 失败会怎样?

A:浏览器会放弃复用 HTML,从头渲染整个 React 树,并报 hydration mismatch 警告。常见原因:服务端和客户端渲染结果不一致(如 Date.now()Math.random()window 引用)。

十一、相关文档