: Next.js 面试 · 渲染模型

第 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 对比总表(背下来)

维度CSRSSRSSGISR
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,而不是删掉重建,因为:

  1. 避免首屏闪白/重排(直接重建会导致内容闪一下)
  2. 省去不必要的渲染成本
  3. 保持首屏内容与 URL 状态一致性

代价是:水合要求客户端生成的虚拟 DOM 树必须与服务端完全一致

5.3 水合不匹配(Hydration Mismatch)

客户端第一次渲染的结果与服务端 HTML 不一致 → React 报错并可能强制客户端重渲染(导致闪动、交互异常)。

常见原因

  • 渲染中直接使用 Date.now() / Math.random() / 浏览器 API(windowlocalStorage
  • 读取 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

实验文件验证目标
CSRcsr/page.tsx + client.tsx浏览器运行时渲染 + 客户端数据请求
SSRssr/page.tsx每次请求重新渲染(刷新时间变化)
SSGssg/page.tsx构建时渲染(时间戳冻结在构建时刻)
ISRisr/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 文件 = 该页被静态化
  • ⚠️ 本项目根 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 判断延迟渲染。

</details> <details> <summary>5. SSG 与 SSR 怎么选?</summary>

看内容是否公开且不常变:公开、低频变化、所有用户相同 → 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 日志的 (静态)/ƒ(动态)是直接证据。

</details> <details> <summary>8. 怎么让一个页面强制动态 / 强制静态?</summary>

强制动态:export const dynamic = "force-dynamic" 或用动态 API / fetch(cache:"no-store")。强制静态:export const dynamic = "force-static"。ISR:export const revalidate = N

</details> <details> <summary>9. 为什么说"Next 里一个页面可以是混合渲染"?</summary>

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 状态保留、并行/拦截路由。