跳到主要内容

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 未定义​

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

解决方案
// 方案一: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 Modules、Tailwind 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: renderToString 和 renderToPipeableStream 有什么区别?​

答案:

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 的 getServerSideProps 和 getStaticProps 有什么区别?​

答案:

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

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

答案:

服务端(Node.js)没有 window、document 等浏览器全局对象。解决方案有三种:(1)使用 typeof window !== 'undefined' 做运行环境判断;(2)将浏览器专属逻辑放在 useEffect 中执行;(3)使用 next/dynamic 的 ssr: 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。

相关链接​