React 服务端渲染(SSR)
服务端渲染是指在服务器上将 React 组件渲染成 HTML 字符串,然后发送给浏览器,而不是由浏览器执行 JavaScript 来生成页面。这样可以加快首屏加载速度,并改善 SEO。
一、核心原理
SSR 的流程分为三个阶段:
- 服务端渲染:使用 ReactDOMServer 将组件渲染成 HTML 字符串
- 发送到客户端:HTTP 响应体中包含完整的 HTML 内容
- 客户端激活(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(手动实现)
更灵活但需要自己处理细节:
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)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 加载和执行:
| 阶段 | CSR | SSR |
|---|---|---|
| 下载 HTML | 100ms | 100ms |
| 下载 JS | 500ms | 500ms |
| 执行 JS | 200ms | 0ms |
| 显示内容 | 800ms | 100ms |
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:
import { hydrateRoot } from 'react-dom/client'
function App() {
return <h1>SSR 应用</h1>
}
// 激活服务端渲染的 HTML
hydrateRoot(document.getElementById('root')!, <App />)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>
}传递初始状态
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>
`
}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 对比
| 特性 | CSR | SSR |
|---|---|---|
| 首屏速度 | 慢 | 快 |
| 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。