FumadocsZDecode
React

React 服务端渲染(SSR)

服务端渲染是指在服务器上将 React 组件渲染成 HTML 字符串,然后发送给浏览器,而不是由浏览器执行 JavaScript 来生成页面。这样可以加快首屏加载速度,并改善 SEO。


一、核心原理

SSR 的流程分为三个阶段:

  1. 服务端渲染:使用 ReactDOMServer 将组件渲染成 HTML 字符串
  2. 发送到客户端:HTTP 响应体中包含完整的 HTML 内容
  3. 客户端激活(Hydration):React 接管静态 HTML,将其转换为交互应用

二、实现方式对比

1. Next.js(推荐)

最简单的方式,开箱即用的 SSR 支持:

// pages/index.tsx
export default function Home({ initialData }: any) {
  return <div>{initialData}</div>
}

// getServerSideProps 在每次请求时在服务器上执行
export async function getServerSideProps() {
  const data = await fetchData()
  return {
    props: {
      initialData: data,
    },
  }
}

优点:配置简单、开发体验好、内置优化 缺点:灵活性受限、依赖 Next.js 框架

2. Express + react-dom/server(手动实现)

更灵活但需要自己处理细节:

server.js
import express from 'express'
import { renderToString } from 'react-dom/server'
import App from './App'

const app = express()

app.get('/', (req, res) => {
  // 服务端渲染组件
  const html = renderToString(<App />)

  // 返回完整的 HTML 页面
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>React SSR</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `)
})

app.listen(3000)
client.js
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import App from './App'

// 激活静态 HTML,使其成为可交互的应用
hydrateRoot(document.getElementById('root'), <App />)

三、SSR 的优势

1. SEO 优化

搜索引擎爬虫接收到的是完整的 HTML 内容,而不需要执行 JavaScript:

<!-- SSR:爬虫能直接看到内容 -->
<div id="root">
  <h1>用户列表</h1>
  <ul>
    <li>用户 1</li>
    <li>用户 2</li>
  </ul>
</div>

<!-- CSR:爬虫只能看到空的 div -->
<div id="root"></div>

2. 更快的首屏加载

用户无需等待 JavaScript 加载和执行:

阶段CSRSSR
下载 HTML100ms100ms
下载 JS500ms500ms
执行 JS200ms0ms
显示内容800ms100ms

3. 减少闪烁

用户在 JS 加载完成前就能看到完整页面,避免由于渲染不一致导致的界面闪烁。


四、关键 API

ReactDOMServer.renderToString()

将组件渲染为 HTML 字符串,包含 React 内部属性(用于后续 hydration):

import { renderToString } from 'react-dom/server'

function MyComponent() {
  return <h1>Hello SSR</h1>
}

const html = renderToString(<MyComponent />)
// <h1 data-reactroot="">Hello SSR</h1>

ReactDOMServer.renderToStaticMarkup()

渲染为纯 HTML,不包含 React 属性,体积更小但无法 hydrate:

import { renderToStaticMarkup } from 'react-dom/server'

function MyComponent() {
  return <h1>Hello SSR</h1>
}

const html = renderToStaticMarkup(<MyComponent />)
// 输出:<h1>Hello SSR</h1>

hydrate() vs createRoot()

服务端渲染的 HTML 用 hydrateRoot 激活,纯客户端应用用 createRoot

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

function App() {
  return <h1>SSR 应用</h1>
}

// 激活服务端渲染的 HTML
hydrateRoot(document.getElementById('root')!, <App />)
client-csr.tsx
import { createRoot } from 'react-dom/client'

function App() {
  return <h1>CSR 应用</h1>
}

// 纯客户端应用用 createRoot
createRoot(document.getElementById('root')!).render(<App />)

五、常见挑战与解决方案

状态同步问题

服务端和客户端的状态必须一致,否则会出现 hydration mismatch:

function App() {
  // ✗ 错误:服务端和客户端的时间不同
  const time = new Date().toLocaleString()

  // ✓ 正确:在客户端 Effect 中获取
  const [time, setTime] = React.useState('')
  React.useEffect(() => {
    setTime(new Date().toLocaleString())
  }, [])

  return <div>{time}</div>
}

传递初始状态

server.ts
import { renderToString } from 'react-dom/server'

interface AppProps {
  initialData: any
}

function App({ initialData }: AppProps) {
  return <div>{initialData}</div>
}

function renderApp(initialData: any) {
  const html = renderToString(<App initialData={initialData} />)
  return `
    <!DOCTYPE html>
    <html>
      <body>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(initialData)}
        </script>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `
}
client.ts
import { hydrateRoot } from 'react-dom/client'

interface AppProps {
  initialData: any
}

function App({ initialData }: AppProps) {
  return <div>{initialData}</div>
}

const initialData = (window as any).__INITIAL_STATE__
hydrateRoot(document.getElementById('root')!, <App initialData={initialData} />)

避免在服务端执行浏览器 API

// ✗ 错误:服务端没有 localStorage
function Component() {
  const data = localStorage.getItem('key')
}

// ✓ 正确:在 useEffect 中访问
function Component() {
  const [data, setData] = React.useState('')
  React.useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setData(localStorage.getItem('key') || '')
  }, [])
}

六、CSR vs SSR 对比

特性CSRSSR
首屏速度
SEO困难良好
服务器负载
开发复杂度简单复杂
缓存友好一般
实时更新简单复杂

何时选择 SSR

  • 需要良好的 SEO(新闻、博客、商城)
  • 首屏加载速度很重要
  • 用户网络环境较差

何时选择 CSR

  • 后台管理系统
  • 内容频繁变化的应用
  • 不需要 SEO

七、现代方案:流式 SSR

React 18+ 支持流式渲染,可以边渲染边发送 HTML:

// Next.js 13+ 自动支持流式 SSR
export default async function Page() {
  const data = await fetch('...')
  return <div>{data}</div>
}

优势:

  • 首字节时间(TTFB)更快
  • 用户更快看到内容
  • 服务器处理 IO 时不阻塞渲染

核心要点:SSR 在服务端渲染 HTML,发送给客户端后用 hydrate 激活为交互应用。优势是首屏快和 SEO 好,但复杂度高。Next.js 是最佳选择,需要更多灵活性时用 Express 手动实现。关键是确保服务端和客户端状态一致,避免 hydration mismatch。

On this page