跳到主要内容

React 服务端渲染

问题

什么是 SSR(服务端渲染)?React 如何实现 SSR?Streaming SSR 带来了哪些改进?SSR 中常见的 Hydration Mismatch 如何解决?

面试速答版

什么是 SSR(服务端渲染)? 服务端把 React 组件渲染成完整 HTML 字符串发给浏览器,用户立即看到内容,再由客户端 JS Hydration(水合)绑事件让页面活起来:

  • 解决 CSR 两大痛点:首屏白屏(要等 JS 下载执行)、SEO 差(爬虫拿到空 HTML)。
  • 代价:服务端有计算成本、TTFB 变慢、需要同构代码、要处理 Node 环境无 window/document 的问题。
  • 兄弟模式:CSR(实时 / 服务器零压力)、SSG(构建期生成 / 最快但数据是快照)、ISR(SSG + 按需 revalidate)。

React 如何实现 SSR? 核心是「服务端 render → 客户端 hydrate」两步:

  • 服务端用 renderToString(<App />)(同步、阻塞)或 renderToPipeableStream(流式、推荐)把组件转 HTML。
  • 把首屏数据序列化进 window.__INITIAL_DATA__,避免客户端再请求一次。
  • 客户端用 hydrateRoot(container, <App data={...} />)不重建 DOM,只比对并绑事件。
  • Next.js 里 Pages Router 用 getServerSideProps,App Router 直接 async function Page() + Server Components。

Streaming SSR 带来了哪些改进? 传统 SSR 是「全有或全无」:要等所有数据 → 全部渲染完 → 全部 JS 加载完 → 全部 hydrate 才能交互。Streaming 把这三步都打散:

  • renderToPipeableStream + <Suspense> 划分边界:Shell 部分立即推给浏览器,慢数据准备好后再流式追加。
  • 选择性 Hydration:每个 Suspense 边界独立 hydrate,用户点哪儿优先 hydrate 哪儿,不用等整页 JS。
  • 结果:TTFB / FCP 大幅下降,慢接口不再拖累整页。

SSR 中常见的 Hydration Mismatch 如何解决? Mismatch 是指客户端首次渲染的虚拟 DOM 和服务端 HTML 对不上,会触发警告甚至 UI 错乱:

  • 常见原因:用了 Date.now() / Math.random() / new Date().toLocaleString() 这种「服务端和客户端结果不一致」的逻辑;直接读 window/localStorage;浏览器扩展改了 DOM;时区/Locale 差异。
  • 处理思路:① 把不一致逻辑放进 useEffect(只在客户端跑);② 用 suppressHydrationWarning 临时屏蔽(最后手段);③ 用 Next.js 的 'use client' + 动态 import 关 SSR:dynamic(() => import('./X'), { ssr: false });④ 数据用服务端注入,避免客户端二次取拿到不同结果。

答案

SSR(Server-Side Rendering) 是指在服务端将 React 组件渲染成 HTML 字符串,将完整的 HTML 发送给浏览器,浏览器先展示静态页面,再通过 Hydration(水合/激活) 让页面具备交互能力。SSR 是解决 SPA 首屏性能和 SEO 问题的核心方案。

一、SSR 概念与动机

为什么需要 SSR

传统 CSR(Client-Side Rendering)的页面初始是一个空的 <div id="root"></div>,所有内容依赖 JavaScript 下载执行后才能渲染。这带来两个核心问题:

  1. 首屏白屏时间长 — 用户需要等待 JS 下载、解析、执行、数据请求完成后才能看到内容
  2. SEO 不友好 — 搜索引擎爬虫抓取到的是空 HTML,无法索引页面内容

SSR 在服务端直接输出完整 HTML,用户可以立即看到页面内容,搜索引擎也能正常抓取。

CSR vs SSR vs SSG vs ISR 对比

特性CSRSSRSSGISR
渲染时机客户端运行时每次请求时构建时构建时 + 按需更新
首屏速度最快
SEO
服务器压力
数据实时性实时实时构建时快照准实时(revalidate)
TTFB较慢最快
适用场景后台管理动态内容页博客/文档电商/新闻
代表框架CRA、ViteNext.js SSRNext.js SSGNext.js ISR
核心理解

SSR 的本质是 时间换空间 — 用服务端计算时间换取用户更快看到内容的时间。但代价是增加了服务器负载和架构复杂度。

二、React SSR 核心 API

React 提供了一组 API 在服务端将组件渲染为 HTML:

API环境特点
renderToStringNode.js同步渲染,返回完整 HTML 字符串
renderToPipeableStreamNode.js流式渲染,支持 Suspense,React 18+
renderToReadableStreamWeb Stream(Edge)流式渲染,适用于 Edge Runtime
hydrateRoot浏览器客户端激活,绑定事件和状态

2.1 renderToString(传统方式)

server.tsx
import { renderToString } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
// 服务端获取数据
const data = await fetchData();

const html = renderToString(<App data={data} />);

res.send(`
<!DOCTYPE html>
<html>
<head><title>SSR App</title></head>
<body>
<div id="root">${html}</div>
<script>
// 将数据序列化到页面,供客户端 hydrate 使用
window.__INITIAL_DATA__ = ${JSON.stringify(data)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
注意

renderToString同步阻塞的 — 整个组件树必须全部渲染完成才会返回。对于大型页面,这会导致 TTFB(Time To First Byte)过长。React 18 推荐使用流式 API 替代。

2.2 Hydration — 客户端激活

服务端返回的 HTML 是静态的,没有事件绑定和状态管理。客户端需要通过 Hydration 让页面"活"过来:

client.tsx
import { hydrateRoot } from 'react-dom/client';
import App from './App';

// 从服务端注入的数据中恢复初始状态
const data = window.__INITIAL_DATA__;

// hydrateRoot 不会重新创建 DOM,而是复用服务端生成的 DOM
// 只绑定事件监听器和恢复 React 内部状态
hydrateRoot(document.getElementById('root')!, <App data={data} />);
Hydration 的工作原理
  1. React 在客户端重新执行组件渲染逻辑(虚拟 DOM)
  2. 将虚拟 DOM 与服务端生成的真实 DOM 进行 比对
  3. 如果匹配,直接 复用 现有 DOM 节点,只绑定事件
  4. 如果不匹配,产生 Hydration Mismatch 警告,可能导致 UI 异常

三、SSR 渲染流程

四、数据获取策略

SSR 中数据获取是最核心的环节之一,需要在服务端完成数据准备后再渲染组件。

服务端获取 vs 客户端获取

策略适用场景优点缺点
服务端获取SEO 关键内容、首屏数据FCP 快、SEO 好增加 TTFB
客户端获取用户交互后的数据、非关键内容服务器压力小用户看到 loading 状态
混合策略大多数场景平衡首屏和交互架构复杂度高
最佳实践

首屏关键数据在服务端获取,非关键数据和用户交互数据在客户端获取。这是目前业界通用的混合策略。

五、React 18 Streaming SSR

React 18 引入的 Streaming SSR 是 SSR 的一次重大升级,解决了传统 SSR 的 "全有或全无" 问题。

5.1 传统 SSR 的问题

传统 SSR 存在三个瓶颈,每一步都必须等上一步全部完成:

  1. 数据获取 — 必须在服务端获取所有数据后才能开始渲染
  2. 渲染 — 必须渲染完整个组件树后才能发送 HTML
  3. Hydration — 必须加载完所有 JS 后才能开始 hydrate,必须 hydrate 完所有组件后页面才能交互

5.2 renderToPipeableStream + Suspense

server.tsx
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/bundle.js'],
onShellReady() {
// Shell(Suspense 边界外的内容)准备好后立即开始流式发送
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onError(error) {
console.error(error);
res.statusCode = 500;
},
}
);
});

在组件中使用 Suspense 划分流式边界:

App.tsx
function App() {
return (
<html>
<body>
<Header /> {/* Shell:立即渲染 */}
<MainContent /> {/* Shell:立即渲染 */}

{/* Suspense 包裹的部分会延迟发送 */}
<Suspense fallback={<CommentsLoading />}>
<Comments /> {/* 数据就绪后流式注入 */}
</Suspense>

<Suspense fallback={<SidebarLoading />}>
<Sidebar /> {/* 另一个独立的流式块 */}
</Suspense>
</body>
</html>
);
}

5.3 选择性 Hydration(Selective Hydration)

React 18 的 Streaming SSR 配合 Suspense 实现了 选择性 Hydration

  • 不再需要等待所有 JS 加载完才开始 hydrate
  • 每个 Suspense 边界可以独立 hydrate
  • 用户交互的区域会被优先 hydrate(紧急 hydration)

5.4 Streaming SSR vs 传统 SSR

特性传统 SSRStreaming SSR
TTFB慢(等所有数据就绪)快(Shell 立即发送)
FCP
渲染方式全量渲染后一次性返回流式分块发送
Hydration全量 Hydration选择性 Hydration
用户交互等全部 hydrate 完成交互区域优先 hydrate
慢数据源影响阻塞整个页面仅阻塞对应 Suspense
关键优势

Streaming SSR 的核心价值在于解耦 — 快的部分先走,慢的部分不拖累整体,用户可以更快看到并操作页面。

六、Next.js SSR 实践

Next.js 是 React SSR 的主流实践框架,提供了两套路由系统。

6.1 Pages Router

pages/posts/[id].tsx
// 每次请求时在服务端执行
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`https://api.example.com/posts/${id}`);
const post = await res.json();

return {
props: { post }, // 传递给页面组件
};
}

export default function Post({ post }) {
return <article>{post.title}</article>;
}
方法执行时机用途
getServerSideProps每次请求SSR,动态内容
getStaticProps构建时SSG,静态内容
getStaticPaths构建时动态路由的 SSG

6.2 App Router(推荐)

App Router 基于 React Server Components,默认所有组件都是 Server Component:

app/posts/[id]/page.tsx
// Server Component — 直接 async/await 获取数据
export default async function PostPage({
params,
}: {
params: { id: string };
}) {
// 无需 getServerSideProps,直接在组件内获取数据
const post = await fetch(`https://api.example.com/posts/${params.id}`, {
cache: 'no-store', // SSR:每次请求获取最新数据
}).then(res => res.json());

return <article>{post.title}</article>;
}

6.3 ISR(增量静态再生成)

ISR 结合了 SSG 的性能和 SSR 的数据实时性:

app/posts/[id]/page.tsx
export default async function PostPage({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.id}`, {
next: { revalidate: 60 }, // 60 秒后重新生成
}).then(res => res.json());

return <article>{post.title}</article>;
}

更多 Next.js 细节请参考 Next.js 核心知识SSR 与 SSG

七、SSR 性能优化

7.1 组件级缓存

对于不依赖用户数据的组件,可以在服务端缓存渲染结果:

server-cache.ts
import LRU from 'lru-cache';

const componentCache = new LRU<string, string>({ max: 1000, ttl: 60_000 });

function renderWithCache(key: string, element: React.ReactElement): string {
const cached = componentCache.get(key);
if (cached) return cached;

const html = renderToString(element);
componentCache.set(key, html);
return html;
}

7.2 流式渲染减少 TTFB

使用 renderToPipeableStream 替代 renderToString,让 Shell 部分立即返回,显著降低 TTFB。

7.3 代码分割与懒加载

配合 React.lazy 和代码分割,减少客户端需要下载的 JS 体积,加速 Hydration:

App.tsx
import { lazy, Suspense } from 'react';

// 重型组件延迟加载,不阻塞首屏
const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
return (
<div>
<Header />
<Suspense fallback={<div>加载图表中...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}

7.4 Edge Runtime

将 SSR 逻辑部署到 Edge(如 Cloudflare Workers、Vercel Edge),利用全球分布的边缘节点减少网络延迟:

app/api/page.tsx
// Next.js App Router 指定 Edge Runtime
export const runtime = 'edge';

export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{/* ... */}</div>;
}

八、SSR 常见问题

8.1 Hydration Mismatch

原因:服务端和客户端渲染的 HTML 不一致。

常见场景:

  • 使用 Date.now()Math.random() 等不确定值
  • 根据 window.innerWidth 等浏览器 API 条件渲染
  • 服务端和客户端的时区、语言设置不同

解决方案

ClientOnly.tsx
// 方案一:useEffect 延迟渲染客户端专属内容
function ClientOnly({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);

// useEffect 只在客户端执行,服务端渲染时 mounted 为 false
useEffect(() => {
setMounted(true);
}, []);

return mounted ? <>{children}</> : null;
}

// 方案二:suppressHydrationWarning(仅用于已知差异)
function Timestamp() {
return (
<time suppressHydrationWarning>
{new Date().toLocaleString()}
</time>
);
}
注意

suppressHydrationWarning 只应用于确实预期不一致的场景(如时间戳),不要滥用它来掩盖真正的 bug。

8.2 window / document 未定义

服务端没有浏览器环境,直接访问 windowdocument 会报错。

解决方案
// 方案一:typeof 守卫
if (typeof window !== 'undefined') {
// 仅在客户端执行
window.addEventListener('resize', handler);
}

// 方案二:动态导入(适用于依赖浏览器 API 的第三方库)
import dynamic from 'next/dynamic';

const MapComponent = dynamic(
() => import('../components/Map'),
{ ssr: false } // 禁止服务端渲染此组件
);

8.3 内存泄漏

SSR 服务是长期运行的 Node.js 进程,需要特别注意内存管理:

  • 避免模块级全局可变状态 — 会在多次请求间共享,造成数据串扰
  • 每次请求创建独立的状态 — 如 Redux Store、数据上下文
  • 及时清理定时器和订阅 — 服务端不会触发 useEffect 清理函数
store.ts
// ❌ 错误:全局单例 Store,所有请求共享
// const store = createStore();

// ✅ 正确:每次请求创建新的 Store
export function createRequestStore() {
return createStore();
}

8.4 CSS-in-JS SSR 适配

CSS-in-JS 库需要在服务端收集样式并注入到 HTML 中:

styled-components SSR
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();
try {
const html = renderToString(sheet.collectStyles(<App />));
const styleTags = sheet.getStyleTags(); // 获取收集到的 CSS
// 将 styleTags 注入到 <head> 中
} finally {
sheet.seal();
}
趋势

由于 CSS-in-JS 在 SSR 中的性能开销和配置复杂度,越来越多的项目转向 CSS ModulesTailwind CSS 等零运行时方案,它们天然支持 SSR 无需额外配置。

九、何时使用 SSR — 决策流程图


常见面试问题

Q1: SSR 和 CSR 的核心区别是什么?

答案

CSR(客户端渲染)在浏览器端执行 JavaScript 渲染页面,初始 HTML 为空壳;SSR(服务端渲染)在服务器上将组件渲染为完整 HTML 再返回给浏览器。核心区别在于首次渲染的执行位置。SSR 的优势是首屏速度快(HTML 直出)和 SEO 友好,劣势是增加了服务器负载和架构复杂度。

Q2: 什么是 Hydration?为什么需要它?

答案

Hydration(水合)是指客户端 React 复用服务端生成的 DOM 节点,给静态 HTML 绑定事件监听器和恢复 React 内部状态的过程。之所以需要 Hydration,是因为 SSR 返回的 HTML 是纯静态的,没有事件绑定、没有状态管理,需要通过 hydrateRoot 让页面具备交互能力。

Q3: Hydration Mismatch 是什么?如何排查和解决?

答案

Hydration Mismatch 是指服务端渲染的 HTML 与客户端 Hydration 时生成的虚拟 DOM 不一致。常见原因:使用 Date.now() 等不确定值、根据 window 对象条件渲染、HTML 嵌套不合法(如 <p> 内嵌 <div>)。解决方案:用 useEffect + useState 延迟渲染客户端专属内容,对已知差异使用 suppressHydrationWarning,确保服务端和客户端的渲染逻辑一致。

Q4: renderToStringrenderToPipeableStream 有什么区别?

答案

renderToString 是同步的,必须等整个组件树渲染完才返回完整 HTML 字符串,大页面会阻塞 TTFB。renderToPipeableStream 是 React 18 的流式 API,配合 Suspense 可以先发送 Shell 部分,等待数据就绪后再流式注入剩余内容,显著降低 TTFB 并支持选择性 Hydration。

Q5: React 18 Streaming SSR 解决了哪些问题?

答案

传统 SSR 的"全有或全无"问题:获取所有数据 → 渲染所有 HTML → 加载所有 JS → Hydrate 所有组件,每步都必须完全完成才能开始下一步。Streaming SSR 通过 Suspense 边界将这些步骤解耦,每个部分独立完成。快的部分先到达用户,慢数据源不阻塞整个页面,用户点击的区域优先 Hydrate。

Q6: 选择性 Hydration 是什么?

答案

选择性 Hydration(Selective Hydration)是 React 18 的特性,配合 Suspense 实现。它允许页面的不同部分独立进行 Hydration,而不必等待所有 JS 加载完成。更进一步,如果用户在 Hydration 完成前就点击了某个区域,React 会优先 Hydrate 该区域(紧急 Hydration),让用户更快获得交互能力。

Q7: Next.js 的 getServerSidePropsgetStaticProps 有什么区别?

答案

getServerSideProps每次请求时在服务端执行,适用于需要实时数据的页面(如个性化内容、频繁更新的数据)。getStaticProps构建时执行,生成静态 HTML,适用于内容较少变化的页面。getStaticProps 还可以配合 revalidate 实现 ISR,在静态生成的基础上按时间间隔重新生成。

Q8: SSR 中如何处理 window is not defined 错误?

答案

服务端(Node.js)没有 windowdocument 等浏览器全局对象。解决方案有三种:(1)使用 typeof window !== 'undefined' 做运行环境判断;(2)将浏览器专属逻辑放在 useEffect 中执行;(3)使用 next/dynamicssr: false 选项动态导入组件,使其仅在客户端渲染。

Q9: SSR 应用如何避免内存泄漏?

答案

SSR 服务器是长期运行的进程,关键注意事项:(1)避免模块级全局可变状态,每次请求应创建独立的状态实例(如 Redux Store);(2)注意闭包引用,确保请求处理函数不会持有对已完成请求的引用;(3)使用连接池管理数据库连接;(4)定期监控 Node.js 进程的堆内存使用。

Q10: ISR(增量静态再生成)的工作原理是什么?

答案

ISR 结合了 SSG 和 SSR 的优势。首次构建时生成静态页面,之后在 revalidate 时间窗口过期后,下一次访问会触发后台重新生成。在重新生成期间,用户仍然看到缓存的旧页面(stale-while-revalidate 策略),新页面生成完成后替换旧缓存。这样既保持了静态页面的高性能,又能定期更新内容。

Q11: Edge Runtime 对 SSR 有什么优势?

答案

Edge Runtime 将 SSR 逻辑部署到全球分布的边缘节点(如 Cloudflare Workers、Vercel Edge Functions),优势:(1)更低的网络延迟 — 就近响应用户请求;(2)冷启动快 — 基于 V8 isolate,毫秒级启动;(3)全球扩展 — 无需手动管理多区域服务器。限制是 Edge 环境的 API 受限(不支持 Node.js 完整 API),适用于轻量 SSR 场景。

Q12: CSS-in-JS 在 SSR 中有什么问题?如何解决?

答案

CSS-in-JS(如 styled-components、Emotion)在 SSR 中需要额外处理:(1)服务端必须收集渲染过程中生成的样式,注入到 HTML 的 <head> 中,否则首屏会出现无样式闪烁(FOUC);(2)增加了服务端渲染的性能开销。解决方案是使用各库提供的 SSR 适配 API(如 ServerStyleSheet),或转向零运行时方案(CSS Modules、Tailwind CSS),后者天然支持 SSR。

Q13: Server Components 和 SSR 是什么关系?

答案

SSR 和 Server Components(RSC)是互补但不同的技术。SSR 是渲染策略 — 在服务端生成 HTML 发送给客户端,组件代码仍然会打包到客户端 JS 中用于 Hydration。RSC 是组件类型 — Server Component 只在服务端运行,其代码不会出现在客户端 Bundle 中。两者结合使用效果最佳:RSC 减少了客户端 JS 体积,SSR(尤其是 Streaming SSR)让 RSC 的输出能更快地到达用户。更多细节见 Server Components 深入

Q14: 如何在 SSR 和 CSR 之间做技术选型?

答案

核心决策依据:(1)SEO 需求 — 需要搜索引擎抓取的内容页面选 SSR/SSG;(2)首屏性能要求 — 对首屏白屏敏感的用户端产品选 SSR;(3)数据实时性 — 静态内容选 SSG/ISR,动态内容选 SSR;(4)服务器预算 — SSR 需要服务器资源,CSR/SSG 只需 CDN;(5)团队能力 — SSR 架构复杂度更高。后台管理系统通常选 CSR,内容型网站选 SSG/ISR,电商/社交等用户端产品选 SSR。

相关链接