渲染模式:CSR / SSR / SSG / ISR / RSC
面试题:CSR、SSR、SSG、ISR、RSC 五种渲染模式的本质区别是什么?Next.js 怎么选?
一、五种模式一句话总结
| 模式 | 全称 | 渲染时机 | 适合场景 |
|---|---|---|---|
| CSR | Client-Side Rendering | 浏览器 | 后台、登录后页面 |
| SSR | Server-Side Rendering | 请求时(服务端) | 个性化、SEO、实时数据 |
| SSG | Static Site Generation | 构建时 | 博客、文档、营销页 |
| ISR | Incremental Static Regeneration | 构建时 + 增量 | 大型电商、商品列表 |
| RSC | React 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 的两大痛点
- TTFB 慢:每个请求都要服务端现渲染
- 必须全量 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 的核心区别
| 维度 | SSR | RSC |
|---|---|---|
| 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 | 每次请求拿最新数据 |
| 后台 / 工具型 SPA | CSR | SEO 无关,强调交互体验 |
| 数据密集型 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 引用)。