2025年08月26日8 分钟

SSR 服务端渲染:原理、流程与 Next.js 实现

深入理解 SSR(Server-Side Rendering)的工作原理,从 HTML 生成到 Hydration 的完整流程

什么是 SSR

SSR(Server-Side Rendering,服务端渲染)是指每次用户请求时,服务器实时执行 React 组件代码,生成完整的 HTML 字符串返回给浏览器。用户立即看到内容,然后 JS 加载完成后"接管"页面的交互能力。

SSR 的完整流程

用户请求 https://example.com/users
        │
        ▼
┌────────────────────────────────────────────────────────┐
│                     服务器端                              │
│                                                         │
│  1. 接收请求 /users                                      │
│  2. 调用 API 获取数据 → [{ name: "Alice" }, ...]        │
│  3. 执行 React 组件:renderToString(<App data={data}/>) │
│  4. 生成完整 HTML 字符串                                  │
└────────────────────┬───────────────────────────────────┘
                     │
                     ▼  服务器返回完整 HTML
┌────────────────────────────────────────────────────────┐
│ <!DOCTYPE html>                                         │
│ <html>                                                  │
│   <body>                                                │
│     <div id="root">                                     │
│       <h1>用户列表</h1>                                  │
│       <ul>                                              │
│         <li>Alice</li>        ← 已有真实内容!            │
│         <li>Bob</li>                                    │
│       </ul>                                             │
│     </div>                                              │
│     <script src="/bundle.js"></script>                   │
│   </body>                                               │
│ </html>                                                 │
└────────────────────┬───────────────────────────────────┘
                     │
                     ▼  用户立即看到内容 ✅(但还不能交互)
                     │
                     │  下载 JS bundle
                     │
                     ▼  JS 下载完成
                     │
                     │  Hydration(水合):
                     │  React 接管已有的 DOM,
                     │  绑定事件监听器
                     │
                     ▼  页面完全可交互 ✅

时间线对比(SSR vs CSR)

CSR:
0ms     200ms      800ms      1500ms     2500ms
│        │          │           │          │
▼        ▼          ▼           ▼          ▼
请求    HTML到达    JS下载完    首次渲染     完整渲染
        ├── 白屏 ──────────────┤

SSR:
0ms     200ms      500ms       1200ms
│        │          │            │
▼        ▼          ▼            ▼
请求    HTML到达    看到内容      JS加载完
        (FCP!)     (已有内容)    (可交互 TTI)
        ├ 可见 ┤

Hydration(水合)详解

Hydration 是 SSR 的核心概念,也是最容易出问题的地方。

服务端渲染的 HTML(静态,无交互):
┌──────────────────────────┐
│ <button>点击 0 次</button> │  ← 只是纯 HTML,点击无反应
└──────────────────────────┘

          Hydration 过程
               │
               ▼

水合后的页面(有交互):
┌──────────────────────────┐
│ <button>点击 0 次</button> │  ← 绑定了 onClick 事件
│   onClick={handleClick}   │
│   state = { count: 0 }   │
└──────────────────────────┘

Hydration 的步骤

// React 水合的内部流程(简化)
function hydrate(component, container) {
  // 1. 不会清空 container 的 DOM(这是和 render 的区别)
  // 2. 遍历已有的 DOM 节点
  // 3. 将 React 组件树和 DOM 节点一一对应
  // 4. 绑定事件监听器
  // 5. 如果 DOM 和组件不匹配 → 发出警告(Hydration Mismatch)
}

水合不匹配问题

// ❌ 会导致 Hydration Mismatch
function TimeDisplay() {
  // 服务端执行时:new Date() = "10:00:00"
  // 客户端执行时:new Date() = "10:00:02"
  return <div>{new Date().toLocaleTimeString()}</div>
  // 服务端 HTML: <div>10:00:00</div>
  // 客户端期望: <div>10:00:02</div>
  // → Mismatch Warning!
}

// ✅ 正确做法:客户端挂载后再显示时间
function TimeDisplay() {
  const [mounted, setMounted] = useState(false)
  const [time, setTime] = useState('')

  useEffect(() => {
    setMounted(true)
    setTime(new Date().toLocaleTimeString())
  }, [])

  if (!mounted) return <div>--:--:--</div>  // 服务端和客户端首次渲染一致
  return <div>{time}</div>
}

Next.js 中的 SSR 实现

App Router(推荐)

// app/users/page.tsx — 默认就是服务端组件

// 这个函数在服务端执行,每次请求时运行
async function UsersPage() {
  // 直接在组件中 fetch 数据(服务端执行)
  const res = await fetch('https://api.example.com/users', {
    cache: 'no-store',  // 关键:no-store = 每次请求都重新获取 = SSR
  })
  const users = await res.json()

  return (
    <div>
      <h1>用户列表</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  )
}

export default UsersPage

Pages Router(旧版)

// pages/users.tsx

export async function getServerSideProps(context) {
  const res = await fetch('https://api.example.com/users')
  const users = await res.json()

  return {
    props: { users },  // 传递给组件
  }
}

function UsersPage({ users }) {
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}

SSR 的优缺点

优点

优点说明
首屏速度快用户立即看到内容,FCP 优秀
SEO 友好爬虫直接获得完整 HTML
社交分享分享链接时能正确显示预览
首次有意义内容快不需要等 JS 下载和执行

缺点

缺点说明
服务器压力大每次请求都要执行渲染
TTFB 较长服务器需要时间准备 HTML
TTI 延迟看到内容到可以交互有时间差
开发复杂度需要处理 Hydration 不匹配
基础设施要求需要 Node.js 服务器,不能纯静态部署

React 18 的流式 SSR

传统 SSR 必须等所有数据到齐才能返回 HTML。React 18 引入了流式 SSR

传统 SSR:
  服务器: [获取数据A] [获取数据B] [渲染HTML] → 一次性发送
  浏览器:  ────等待──────────────────────── 收到完整HTML

流式 SSR (React 18):
  服务器: [渲染HTML外壳] → 立即发送
          [获取数据A] [渲染A] → 追加发送
          [获取数据B] [渲染B] → 追加发送
  浏览器: 收到外壳 → 显示  → 收到A → 更新 → 收到B → 更新
// Next.js App Router 自动支持流式 SSR
import { Suspense } from 'react'

async function SlowComponent() {
  const data = await fetch('/api/slow')  // 耗时 3 秒
  return <div>{data}</div>
}

export default function Page() {
  return (
    <div>
      <h1>立即显示的标题</h1>
      <Suspense fallback={<Loading />}>
        <SlowComponent />  {/* 3 秒后流式推送到客户端 */}
      </Suspense>
    </div>
  )
}
// 用户立即看到标题和 Loading,3秒后 Loading 被替换为真实内容

适用场景

✅ 适合 SSR:
  ├── 需要 SEO 的动态内容页面
  ├── 电商商品详情页(价格实时变化 + 需要 SEO)
  ├── 社交媒体的 Feed 流
  ├── 新闻/资讯网站
  └── 需要个性化内容(用户信息、推荐)

❌ 不适合 SSR:
  ├── 纯静态内容(用 SSG 更好)
  ├── 高并发场景(服务器扛不住)
  ├── 不需要 SEO 的内部系统
  └── 交互为主、内容为辅的应用

总结

SSR 解决了 CSR 的白屏和 SEO 问题,代价是更高的服务器负担和开发复杂度。React 18 的流式 SSR 和 Suspense 大幅改善了传统 SSR 的 TTFB 问题,是当前 Web 应用的主流渲染方案之一。