react 之服务端渲染(SSR)

目录

  • 前言
  • [一、React SSR 的概念](#一、React SSR 的概念)
  • [二、React SSR 的核心原理](#二、React SSR 的核心原理)
    • [1、服务端渲染 React 组件](#1、服务端渲染 React 组件)
    • [2、将 HTML 注入模板返回给浏览器](#2、将 HTML 注入模板返回给浏览器)
    • [3、客户端 hydration](#3、客户端 hydration)
  • [三、React SSR 的典型流程](#三、React SSR 的典型流程)
    • [1、完整 React SSR 渲染流程](#1、完整 React SSR 渲染流程)
    • [2、面试必会:简述 React SSR 渲染流程(⭐️⭐️⭐️)](#2、面试必会:简述 React SSR 渲染流程(⭐️⭐️⭐️))
  • [四、React SSR 的数据获取方案(重点 & 难点)](#四、React SSR 的数据获取方案(重点 & 难点))
  • [五、React SSR 的关键 API](#五、React SSR 的关键 API)
  • [六、React SSR + React Router](#六、React SSR + React Router)
  • [七、React SSR 的常用框架](#七、React SSR 的常用框架)
  • [八、React SSR 架构拆分(工程级)](#八、React SSR 架构拆分(工程级))
  • [九、React SSR 的性能优化](#九、React SSR 的性能优化)
  • [十、React SSR 实战要点](#十、React SSR 实战要点)
  • [十一、React SSR 的常见坑(非常重要)](#十一、React SSR 的常见坑(非常重要))
    • [1、hydration mismatch](#1、hydration mismatch)
    • [2、直接访问浏览器 API](#2、直接访问浏览器 API)
    • [3、useLayoutEffect 警告](#3、useLayoutEffect 警告)
    • 4、样式闪烁(FOUC)
  • [十二、SSR 与 CSR、SSG 的全面对比](#十二、SSR 与 CSR、SSG 的全面对比)

前言

学习 React SSR 可与 Vue 之服务端渲染(SSR)对比学习(效率更高),虽然 Vue 和 React 框架不同,但 SSR 原理都是相通的。

一、React SSR 的概念

React SSR(Server-Side Rendering) 指的是:

  • 在**服务端(Node.js)**运行 React 组件,将其渲染成 完整 HTML 返回给浏览器,然后在客户端执行 hydration,让静态 HTML 变成可交互的 React 应用。
  • 简而言之:React SSR = 服务端先渲染页面结构,客户端再"接管"它

SSR(服务端渲染)对比 CSR(客户端渲染):

特性 CSR SSR
HTML 初始内容 空或 minimal shell 完整 HTML 内容
首屏渲染时间 较慢(需下载 JS 再渲染) 较快(HTML 已在服务端生成)
SEO 支持 较差 较好(搜索引擎爬虫可直接抓取 HTML)
数据获取 客户端 fetch 服务端 fetch 或客户端 fetch
交互 完全由客户端控制 初次渲染由服务端生成,交互由客户端接管(hydration)

二、React SSR 的核心原理

React SSR 的核心在于 同一套 React 组件,在两个环境中执行:

环境 职责
Node.js 渲染 HTML
Browser 绑定事件 + 管理状态

核心机制(render + hydrate)------在服务端调用 React 组件生成 HTML 字符串:

  • 服务端渲染 React 组件
  • 将 HTML 注入模板返回给浏览器
  • 客户端 hydration

1、服务端渲染 React 组件

typescript 复制代码
import { renderToString } from 'react-dom/server';
import App from './App';

const html = renderToString(<App />);

2、将 HTML 注入模板返回给浏览器

typescript 复制代码
const template = `
<html>
  <head><title>SSR Demo</title></head>
  <body>
    <div id="root">${html}</div>
    <script src="/bundle.js"></script>
  </body>
</html>
`;

res.send(template);

3、客户端 hydration

typescript 复制代码
import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(document.getElementById('root'), <App />);

三、React SSR 的典型流程

1、完整 React SSR 渲染流程

typescript 复制代码
浏览器请求 URL
        ↓
Node 服务接收请求
        ↓
创建 React 应用实例
        ↓
服务端路由匹配(StaticRouter)
        ↓
服务端数据预取
        ↓
renderToString / renderToPipeableStream
        ↓
生成 HTML
        ↓
注入模板 + 初始数据
        ↓
返回浏览器
        ↓
浏览器展示首屏
        ↓
客户端下载 JS
        ↓
hydrate(React 接管 DOM)
        ↓
后续进入 SPA 模式

⚠️ SSR 只负责首屏,之后就是纯 SPA

2、面试必会:简述 React SSR 渲染流程(⭐️⭐️⭐️)

  • 客户端请求 URL
  • 服务端根据 URL 渲染 React 组件生成 HTML
  • 服务端返回完整 HTML 给浏览器
  • 浏览器渲染 HTML, 展示首屏(HTML 即可)
  • React 客户端 JS 下载并执行 hydrate,接管页面交互(进入 SPA)
  • 后续操作可以通过客户端路由(如 React Router)处理,无需再回服务端渲染

四、React SSR 的数据获取方案(重点 & 难点)

在 SSR 场景下,页面在服务端渲染时就需要完成 数据获取和模板渲染,因此有几个核心问题:

  • 服务端渲染时需要数据:普通 React SPA 组件通常在 useEffect 或 componentDidMount 中获取数据,但 SSR 时这些生命周期不执行。
  • 服务端获取的数据如何注入 HTML:生成的 HTML 需要包含渲染所需数据,确保客户端 Hydration 后页面状态一致。
  • 客户端与服务端数据一致性:避免 Hydration 警告或重复请求。
方面 重点 难点
数据获取 组件静态方法(fetchData / getInitialProps 路由匹配组件层级深,如何保证所有异步请求完成
数据注入 服务端渲染完成后,将初始状态注入 HTML 数据序列化性能和安全(XSS 防护)
客户端复用 Hydration 直接使用注入状态 防止重复请求或状态不一致
异步依赖处理 多组件并行/串行请求 异步请求顺序和依赖管理
错误处理 异步请求失败 → 返回 500 或 fallback 如何优雅降级,不阻塞 SSR

1、获取数据

(1)、方式一:在生命周期中获取(不推荐)

方法:在 componentDidMount 或 useEffect 中请求数据

问题:

  • 服务端渲染不会执行这些生命周期
  • 初始 HTML 渲染时没有数据 → 页面空白 → SEO 不友好

结论:无法解决 SSR 数据预取问题,只适合客户端补数据。

(2)、方式二:服务端数据预取(React 官方推荐)(⭐️⭐️⭐️⭐️⭐️)

在 React SSR 中,通常做法分以下四步走:

  1. 组件提供静态数据获取接口
    • React 生态中常用:getInitialProps(Next.js)或"自定义静态方法"------例如 Component.fetchData(store, params)
  2. 服务端统一收集组件数据
    • 在服务端渲染前,遍历匹配的路由组件,执行所有数据请求
  3. 等待所有 Promise 完成后渲染
    • 渲染 HTML 时数据已准备好
  4. 注入数据到模板
    • 在 <script> 标签中注入初始数据,客户端 Hydration 时复用
①、自定义静态方法获取 SSR 数据
typescript 复制代码
// React 组件
function Post({ post }) {
  return <div>{post.title}</div>;
}

// 静态方法获取数据(重要)
Post.fetchData = (params) => fetch(`/api/posts/${params.id}`).then(res => res.json());

// 服务端渲染
async function render(url) {
  const matchedComponents = matchRoutes(routes, url).map(c => c.component);
  const promises = matchedComponents
    .filter(c => c.fetchData)
    .map(c => c.fetchData({ id: 123 }));

  const data = await Promise.all(promises);

  // 将数据注入模板
  const html = ReactDOMServer.renderToString(<App initialData={data} />);
  return `
    <div id="root">${html}</div>
    <script>window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script>
  `;
}

核心点:服务端统一收集数据 → 渲染 HTML → 注入模板 → 客户端 Hydration

另外,在 SSR 中,组件通常需要服务端数据(API 数据):

  • 在服务端 fetch 数据
  • 客户端再 fetch 数据:
    • 优点:逻辑统一
    • 缺点:首屏可能白屏或闪烁

2、数据注入模板(⭐️⭐️⭐️⭐️⭐️)

注入模板主要目的是:

  • 服务端渲染 HTML 时直接使用数据
  • 客户端 Hydration 时复用数据,避免重复请求
html 复制代码
<div id="root">${html}</div>
<script>
  window.__INITIAL_DATA__ = {...}  <!-- 服务端注入 -->
</script>

客户端使用:

typescript 复制代码
const initialData = window.__INITIAL_DATA__;
const root = document.getElementById('root');
ReactDOM.hydrate(<App initialData={initialData} />, root);

重点:保证服务端和客户端的数据一致,避免 Hydration 报错或二次请求

五、React SSR 的关键 API

API 作用 备注
renderToString 服务端生成 HTML 字符串 不包含事件绑定
renderToStaticMarkup 服务端生成静态 HTML,无额外 React 属性 SEO 优化,减少额外属性
hydrateRoot 客户端接管服务端渲染的 HTML React 18+ 推荐用法
Suspense 服务器端支持异步组件 React 18 SSR 支持并发模式
renderToPipeableStream React 18 新增 SSR 流式渲染 支持首屏更快渲染和可中断流

React 18 引入了流式 SSR,使得 首屏渲染速度 和 时间到交互(TTI) 更优秀。

六、React SSR + React Router

React Router v6 支持 SSR。

服务端:

typescript 复制代码
import { StaticRouter } from 'react-router-dom/server'

<StaticRouter location={req.url}>
  <App />
</StaticRouter>

客户端:

typescript 复制代码
import { BrowserRouter } from 'react-router-dom'
环境 Router
服务端 StaticRouter
客户端 BrowserRouter

七、React SSR 的常用框架

1、Next.js(绝对主流)

  • React 官方推荐
  • 内置:
    • SSR / SSG / ISR
    • 路由
    • 数据获取
    • SEO
    • Edge SSR
typescript 复制代码
export default function Page({ data }) {}

2、其他方案

框架 特点
Remix 数据优先
Razzle 零配置
自建 灵活但复杂

八、React SSR 架构拆分(工程级)

标准三入口结构:

typescript 复制代码
src/
 ├── App.tsx
 ├── entry-client.tsx
 └── entry-server.tsx

entry-server.tsx":

typescript 复制代码
const html = renderToString(
  <StaticRouter location={url}>
    <App />
  </StaticRouter>
)

entry-client.tsx:

typescript 复制代码
hydrateRoot(
  document.getElementById('root'),
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

九、React SSR 的性能优化

1、流式渲染

  • renderToPipeableStream 支持边渲染边发送 HTML
  • 可配合 Node.js res.write 实现更快首屏

renderToPipeableStream 流式渲染:

typescript 复制代码
renderToPipeableStream(<App />, {
  onShellReady() {
    stream.pipe(res)
  }
})

2、缓存

(1)、静态资源缓存

  • SSR HTML 可做短期缓存
  • 静态 JS/CSS 可 CDN 缓存

(2)、服务端缓存

  • 页面渲染结果可缓存到 Redis 或内存,减少重复渲染开销

3、减少 hydration 成本

  • 避免不必要的状态
  • 合理拆 Suspense 边界

十、React SSR 实战要点

  • 每个请求必须是新 React 树
  • 服务端与客户端代码必须一致
  • 数据必须在 render 前准备好
  • 状态必须注入给客户端
  • 优先使用框架(Next.js)
  • SSR 只做首屏

十一、React SSR 的常见坑(非常重要)

1、hydration mismatch

❌ 错误再现:

typescript 复制代码
Math.random()
Date.now()

✔ 正确:

typescript 复制代码
useEffect(() => setTime(Date.now()), [])

2、直接访问浏览器 API

SSR 阶段只负责"算 HTML 字符串",不是"跑页面"。

❌ 下面这些 API 在 React SSR 中「绝对不能直接用」:

typescript 复制代码
window
document
localStorage

✅ 标准写法(必须牢记)

typescript 复制代码
if (typeof window !== 'undefined') {
  // 浏览器环境
}

✅ 或者 放进 useEffect(推荐),例如:

typescript 复制代码
useEffect(() => {
  const token = localStorage.getItem('token')
}, [])

useEffect 永远不会在 SSR 执行。

3、useLayoutEffect 警告

useLayoutEffect → SSR 下会报警。

✔ 替代:

typescript 复制代码
useEffect

4、样式闪烁(FOUC)

  • CSS 未提前注入
  • Next.js / CSS-in-JS 需配置 SSR

十二、SSR 与 CSR、SSG 的全面对比

对比维度 CSR(客户端渲染) SSR(服务端渲染) SSG(静态站点生成)
渲染发生位置 浏览器执行 JS 渲染 服务器实时渲染 HTML 构建阶段预生成 HTML
渲染时机 页面加载后由 JS 渲染 每次请求动态生成 构建时一次性生成
首屏速度 🐢 最慢(JS 执行后才出内容) 👍 快(服务器已渲染) 🚀 最快(CDN 直接返回 HTML)
SEO 效果 ⭐(较差,依赖 JS) ⭐⭐⭐⭐(完美) ⭐⭐⭐⭐(完美)
页面动态能力 ⭐⭐⭐(前端自由拉 API) ⭐⭐⭐⭐(实时内容) ⭐(内容固定,需重构建或 ISR)
服务器压力 0(仅 API 压力) 高(每次访问都需渲染) 0(纯静态资源)
部署成本 低(静态资源即可) 高(需要 Node 服务) 超低(CDN 即可)
可扩展性 ⭐⭐⭐⭐(静态资源无限扩展) ⭐⭐(依赖服务器扩容) ⭐⭐⭐⭐(CDN 无限扩展)
用户交互体验 ⭐⭐⭐⭐(SPA 最强体验) 中等 中等
工程复杂度 高(所有逻辑在前端) 简单
构建速度 慢(页面越多越慢)
适合项目类型 工具型 WebApp、后台系统 电商、新闻、动态内容 + SEO 博客、文档、官网
典型场景 SaaS、仪表盘、在线编辑器 商品页、新闻页、用户中心 内容稳定、偶尔更新的网站
代表框架与模式 React/Vue SPA + fetch API 1、Next.js - getServerSideProps 2、Nuxt.js - asyncDatad 1、Next.js getStaticProps 2、Nuxt.js - asyncData
优点总结 强交互、低成本、高灵活度 首屏快、SEO 强、动态内容强 极快、极便宜、SEO 佳、无服务器
缺点总结 首屏慢、SEO 差 成本高、服务器压力大 不适合经常变化的内容