第 1 章 · 渲染模型:SSR / CSR / SSG / ISR 与水合
本章主线一句话:页面的 HTML 和 JS 在"什么时间、什么地点"被生成,决定了 SEO、首屏速度、数据新鲜度与交互成本——Next 的核心能力就是让你在同一个项目里按页灵活切换这些模式。
1.1 先建立坐标:渲染的两个维度
理解四种模式,先抓住两个轴:
| 轴 | 问题 | 取值 |
|---|---|---|
| 时间 | HTML 什么时候生成? | 构建时(build time) / 请求时(request time) / 客户端运行后 |
| 地点 | HTML 在哪生成? | 服务器 / 浏览器 |
- CSR:浏览器运行时生成(客户端)
- SSR:每次请求时在服务器生成
- SSG:构建时在服务器生成(一次)
- ISR:构建时生成 + 过期后按周期在服务器重新生成
记忆锚点:SSR/SSG/ISR 的 HTML 都来自服务器,区别只在生成时机;CSR 则整个页面由浏览器现场拼。
1.2 四种渲染模式逐个拆解
2.1 CSR(客户端渲染)
流程:
浏览器请求 → 服务器返回一个几乎空的 HTML(<div id="root"> + <script>)
→ 浏览器下载并执行 JS bundle
→ React 在浏览器里渲染出页面
→ 数据在浏览器里 fetch
优点:开发体验好(纯前端)、后续交互流畅(无需往返服务器)、服务器压力小。 缺点:
- SEO 差(爬虫看到空 HTML;即使支持 JS 渲染的爬虫也慢)
- 首屏慢(先下 JS → 执行 → 渲染 → 再请求数据 → 二次渲染)
- 弱网/低端设备体验差
2.2 SSR(服务端渲染)
流程:
浏览器请求 → 服务器运行 React 组件,生成完整 HTML 直接返回
→ 浏览器立刻能看到内容(首屏快)
→ 同时下载 JS bundle
→ 浏览器执行 JS,"水合"HTML,页面才可交互
关键认知(面试常考):SSR 不等于页面可交互。服务端返回的 HTML 是"死的"——没有事件、没有状态,必须等 JS 下载执行完、水合完成才能交互。所以:
- SSR 提升的是 TTFB / FCP(内容可见快)
- 不直接提升 TTI(可交互时间由 JS 体积决定)
- SSR 每次请求都跑一遍服务端渲染 → TTFB 有服务端计算开销,高并发下有成本;数据实时性最好
优点:SEO 好、首屏内容快、数据在服务端获取(安全、少一次往返)。 缺点:每次请求都要重新渲染(性能开销)、服务器负载高、TTI 仍受 JS 影响。
2.3 SSG(静态站点生成)
流程:
构建时:服务器运行一次组件,生成静态 HTML 文件
→ 部署后,任何请求直接返回文件(CDN 可缓存)
→ 数据"冻结"在构建那一刻
优点:性能最好(纯静态文件、CDN 缓存、零服务器开销)、SEO 好、可用性最高。 缺点:数据不新鲜——发布后内容变了,不会自动更新;不适合个性化内容。
适用:博客、文档站、营销页、帮助中心、公开目录页等"所有用户看到相同内容"的页面。
2.4 ISR(增量静态再生成)
流程:
构建时生成静态 HTML(同 SSG)
→ 请求时如果已超过 revalidate 周期:
先返回缓存的旧 HTML(用户几乎无感)
后台异步重新渲染,生成新 HTML 替换旧缓存
→ 过期前所有请求直接命中缓存
关键认知:
- ISR = SSG + 定期"过期再生成",思路是 stale-while-revalidate(SWR):旧数据先顶上,后台刷新
revalidate: 10表示"最多每 10 秒重新生成一次",不是"每 10 秒一定更新"——只有过期后的首次请求才触发重建,且重建期间用户拿到的是旧版本- 注意:Next 中 ISR 的
revalidate也依赖 数据请求本身的缓存(见第 4 章,Next 15+ fetch 默认不缓存,要用next.revalidate配合)
优点:兼顾静态性能 + 数据定期刷新。 缺点:数据不是严格实时(最多延迟一个周期);个性化场景仍不适合。
1.3 对比总表(背下来)
| 维度 | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| HTML 生成地点 | 浏览器 | 服务器 | 构建时 | 构建时 + 后台周期重建 |
| HTML 生成时机 | 运行时 | 每次请求 | 构建时一次 | 构建时 + 过期后重建 |
| 数据新鲜度 | 实时 | 实时 | 构建时冻结 | 周期级(非严格实时) |
| SEO | 差 | 好 | 好 | 好 |
| 首屏(FCP) | 慢 | 快(服务端算完才返回) | 最快 | 快 |
| 服务器开销 | 低 | 高(每次渲染) | 零 | 低(周期重建) |
| 可交互(TTI) | JS 下完即好 | 依赖 JS 水合 | 依赖 JS 水合 | 依赖 JS 水合 |
| 个性化/登录态 | 天然适合 | 适合 | 不适合 | 不适合 |
| Next 默认用法 | 'use client' + 动态 | 动态 API / force-dynamic | 默认(无动态 API) | export const revalidate |
四者关系图(面试直接画):
静态内容(公开) 动态内容(个性化/实时) ┌─────────────────────────────┐ ┌─────────────────────────┐ │ SSG(不变) │ │ SSR(每次实时渲染) │ │ ISR(定期变) │ │ CSR(纯客户端) │ └─────────────────────────────┘ └─────────────────────────┘ ← 左侧性能优先 → ← 右侧实时性优先 →
1.4 选型判断逻辑(决策树)
按优先级问三个问题:
Q1:页面需要 SEO / 首次内容要能被搜索引擎或社交爬虫抓到吗?
└─ 不需要 → 纯交互应用 / 后台 → CSR(或 SSR 兜底)
└─ 需要 → 走 Q2
Q2:内容是否因人而异(登录态、个性化、地域)?
└─ 是 → SSR(+ 客户端交互部分用 CSR 组件)
└─ 否 → 走 Q3
Q3:数据变化多快?是否需要"接近实时"?
└─ 几乎不变(博客/文档)→ SSG
└─ 定期变化(商品价格/榜单/新闻)→ ISR
└─ 必须实时(行情/监控)→ SSR(必要时 + CSR 客户端轮询)
混合使用:Next App Router 允许同一应用里不同路由用不同模式,这是它的核心优势——一个页面也可以"骨架静态 + 局部动态"(第 3 章 RSC / 第 6 章流式渲染会深入)。
1.5 水合 Hydration(本章最常考)
5.1 是什么
服务端渲染返回的 HTML 只是静态快照:能看到字,但没有事件、没有状态、不能交互。
水合 = 让 React 在客户端"接管"这段服务端生成的 HTML:
服务端 HTML ──(下载 JS 并执行)──> React 在客户端重建组件树
──> 与已有 DOM 匹配(复用,不重绘)
──> 附加事件处理器、恢复状态(hooks、useState)
──> 页面变为可交互
5.2 为什么不能直接"清空重渲染"
水合复用了服务端生成的 DOM,而不是删掉重建,因为:
- 避免首屏闪白/重排(直接重建会导致内容闪一下)
- 省去不必要的渲染成本
- 保持首屏内容与 URL 状态一致性
代价是:水合要求客户端生成的虚拟 DOM 树必须与服务端完全一致。
5.3 水合不匹配(Hydration Mismatch)
客户端第一次渲染的结果与服务端 HTML 不一致 → React 报错并可能强制客户端重渲染(导致闪动、交互异常)。
常见原因:
- 渲染中直接使用
Date.now()/Math.random()/ 浏览器 API(window、localStorage) - 读取 cookies 的初值不一致(服务端读到的请求 cookie vs 客户端读到的本地 cookie)
- 主题/时区/地区相关渲染(服务端不知道用户浏览器设置)
- 第三方组件在客户端才渲染(如
mounted判断)
处理方式:
// 方式一:等客户端确定后再渲染(最稳,会延迟首屏渲染该部分)
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <Skeleton />;
return <ClientOnlyValue />;
// 方式二:对"有意为之"的差异用 suppressHydrationWarning(仅限该节点,不是无脑关闭)
// 典型:html 标签上的主题 class
<html suppressHydrationWarning>
排查:看控制台水合警告里给出的"出错的具体节点和差异",不要整个页面用 suppressHydrationWarning 糊掉。
5.4 性能指标视角(一句话总结)
SSR/SSG 解决:TTFB ↓、FCP ↓(内容快)
水合决定: TTI(什么时候能点)
JS 体积决定:水合要多快 → 所以 Next 的 next/dynamic、RSC 都服务于"少传 JS"
面试加分点:能说出 "SSR 只解决'看到内容','能交互'取决于水合与 JS 体积",并引出流式渲染(SSR 不必等整页算完再返回,可以分段吐给浏览器,第 6 章展开)。
1.6 Next 中如何控制(对应代码)
| 意图 | 写法 |
|---|---|
| 强制动态(每次请求渲染) | export const dynamic = "force-dynamic",或使用动态 API |
| 强制静态(构建时渲染) | export const dynamic = "force-static" |
| ISR(定期重建) | export const revalidate = 60 |
| 触发 API 路径重建 | revalidatePath("/path") / revalidateTag("tag")(第 4 章详讲) |
| 客户端组件 | 文件顶部 "use client"(第 3 章详讲) |
Next 的默认行为(重点,容易踩坑):
- App Router 中,没有使用任何动态 API 的路由段默认静态渲染(相当于 SSG)
- 下列情况会让路由变为动态渲染:
- 使用
cookies()、headers()、searchParams(读取 URL 参数)、request等动态 API - 使用
fetch并显式cache: "no-store"或next: { revalidate: 0 }(Next 15+ fetch 默认就不缓存,见第 4 章) - 显式
export const dynamic = "force-dynamic"
- 使用
- 父级 layout 用了动态 API,会把整棵子树拖成动态(本项目根 layout 就用了
cookies(),见实验说明) - 实测补充(Next 16 / 本项目 build 日志):
- 子页面显式
export const dynamic = "force-static"→ 可以覆盖父级动态,页面仍标记为静态(○) - 但仅写
export const revalidate = N(ISR)→ 不能覆盖,页面仍被父级拖成动态(ƒ) - 原因:ISR 仍依赖"静态渲染"这条链,父级动态 API 使整链动态;
force-static是显式断言"本页静态",优先级更高
- 子页面显式
1.7 代码实验
位置:src/app/lab/rendering/,入口 /lab/rendering。
| 实验 | 文件 | 验证目标 |
|---|---|---|
| CSR | csr/page.tsx + client.tsx | 浏览器运行时渲染 + 客户端数据请求 |
| SSR | ssr/page.tsx | 每次请求重新渲染(刷新时间变化) |
| SSG | ssg/page.tsx | 构建时渲染(时间戳冻结在构建时刻) |
| ISR | isr/page.tsx | 每 revalidate 周期后台重建 |
| 水合 | hydration/page.tsx + client.tsx | 事件恢复、window undefined、mismatch 演示 |
观察方法(重要):
pnpm dev:所有页面每次请求都会重新渲染 → 看不到静态效果,属正常pnpm build && pnpm start:- build 日志会打印每个路由的标记(末尾有图例):
○= Static(静态预渲染,prerendered as static content)●= SSG(使用generateStaticParams的静态 HTML)ƒ= Dynamic(按需服务端渲染)
.next/server/app/.../*.html里存在静态 HTML 文件 = 该页被静态化
- build 日志会打印每个路由的标记(末尾有图例):
- ⚠️ 本项目根 layout 的坑(实测):
src/app/layout.tsx使用了await cookies()(Next 动态 API),会把大多数子页面拖成动态。但本项目pnpm build实测结果(Next 16):ssg页(force-static)→ build 日志标记○(Static),无需任何处理即可观察到静态效果isr页(仅revalidate = 10)→ build 日志标记ƒ(Dynamic),ISR 被父级动态 API 抵消- 想观察"干净的"ISR:临时注释根 layout 的 cookies 逻辑 →
pnpm build看标记 → 看完还原 - 这个差异本身就是一个考点:force-static 是"显式断言",revalidate 只是"缓存提示"
1.8 面试自测题
<details> <summary>1. SSR 和 CSR 的核心区别?</summary>渲染时机与地点不同:CSR 由浏览器运行时渲染(HTML 几乎为空),SSR 由服务器每次请求时渲染好完整 HTML 返回。SSR 首屏可见快、SEO 好,但服务器有计算开销;CSR 首屏慢、SEO 差,但后续交互无需往返。
</details> <details> <summary>2. "SSR 让页面更快"这个说法对吗?为什么?</summary>不完整。SSR 只让内容更快可见(TTFB/FCP),可交互时间(TTI)仍取决于客户端 JS 体积与水合速度。且 SSR 每次请求都重新渲染,TTFB 本身有服务端开销,高并发下可能比静态页更慢。
</details> <details> <summary>3. 什么是水合?为什么需要水合?</summary>服务端返回的 HTML 是静态快照,没有事件和状态。水合是 React 在客户端重建组件树并与已有 DOM 匹配、附加事件、恢复状态的过程。需要水合是为了复用服务端 HTML(避免闪白与重复渲染)并恢复交互。
</details> <details> <summary>4. 水合不匹配怎么发生的?怎么排查?</summary>客户端首次渲染结果与服务端 HTML 不一致,常见于渲染中用了 Date.now()、Math.random()、window 等浏览器态,或 cookie/主题/时区初值不一致。排查看控制台警告定位具体节点;对"有意差异"用 suppressHydrationWarning(仅该节点),否则用 mounted 判断延迟渲染。
看内容是否公开且不常变:公开、低频变化、所有用户相同 → SSG(性能最好);需要 SEO + 内容与请求/用户相关 → SSR。进一步,公开但会定期变化 → ISR。
</details> <details> <summary>6. ISR 与 SSG、SSR 的关系?</summary>ISR = SSG + 定期后台重建。构建时生成静态页,过期后首次请求先返回旧缓存并异步重建新页。介于 SSG 与 SSR 之间:有静态的性能与可用性,又有接近实时的数据。注意它不是严格实时(最多延迟一个 revalidate 周期)。
</details> <details> <summary>7. Next(App Router)里一个页面默认是静态还是动态?</summary>默认静态(SSG 式):只要该路由段及其依赖没使用动态 API(cookies/headers/searchParams 等)、没禁用缓存。Next 15+ 中 fetch 默认不缓存,所以用了未缓存 fetch 的页面会变动态。build 日志的 ○(静态)/ƒ(动态)是直接证据。
强制动态:export const dynamic = "force-dynamic" 或用动态 API / fetch(cache:"no-store")。强制静态:export const dynamic = "force-static"。ISR:export const revalidate = N。
App Router 允许不同路由段用不同渲染模式;同一页面里,服务端组件负责静态/动态内容,客户端组件负责交互。加上 RSC 与流式渲染,可做到"骨架静态、局部动态"(后两章展开)。
</details> <details> <summary>10. SEO 角度,四种模式怎么排?</summary>SSG = ISR > SSR > CSR。SSG/ISR/SSR 都返回完整 HTML,SEO 无差别;CSR 返回空 HTML,对爬虫最不友好(前提是爬虫不执行 JS)。
</details> <details> <summary>11. 一个"实时性要求高"的页面(如行情)用什么?</summary>主体用 SSR 保证 SEO 与首屏,实时数据用客户端组件轮询 / WebSocket 局部刷新(不能依赖 ISR,它非实时)。
</details> <details> <summary>12. SSR 一定比 CSR 快吗?</summary>不一定。SSR 的 HTML 生成有服务端开销(TTFB 可能更慢);且交互能力依赖水合。如果页面内容与 SEO 无关且用户不首访(如登录后的仪表盘),CSR 可能更合适。
</details>1.9 常见追问(加分项)
- "水合为什么报错后整个页面可能重渲染?":React 检测到不匹配后,为保证 UI 一致性会回退到客户端全量渲染(
hydrateRoot→ 类似createRoot行为),导致首屏内容闪动。 - "为什么 Next 15 起 fetch 默认不缓存?":避免"服务端组件用了 fetch 却没拿到新数据"的隐式缓存陷阱,把缓存决策显式化(详见第 4 章)。
- "SSR 时
window为什么 undefined?":服务端没有浏览器对象,Server 组件/SSR 阶段在 Node 环境执行,访问window即抛错;这也正是第 3 章"Server ↔ Client 边界"的入口。
下一章:第 2 章 App Router 路由 —— 文件系统路由、匹配原理、layout 状态保留、并行/拦截路由。
