说说 React 服务端渲染怎么做?原理是什么?

一、是什么

服务端渲染(Server-Side Rendering,SSR)是指将 React 组件在服务器端执行渲染,生成完整的 HTML 字符串发送到浏览器,浏览器接收后可以直接展示内容,然后再通过水合(Hydration)将 HTML 与客户端 JavaScript 绑定,使页面具备交互能力。

与传统的客户端渲染(CSR)对比:

特性CSRSSR
首屏渲染需等 JS 下载执行后才有内容服务端直出 HTML,立即可见
SEO搜索引擎难以抓取动态内容HTML 已包含完整内容,SEO 友好
服务器压力低(只提供静态资源)高(每个请求都要渲染)
交互就绪时间(TTI)FCP 和 TTI 接近FCP 快但 TTI 可能延后

二、基本原理与实现

核心 API

React 提供了专门的服务端渲染 API,位于 react-dom/server

// React 18 之前
import { renderToString } from 'react-dom/server';

// React 18 推荐
import { renderToPipeableStream } from 'react-dom/server';

renderToString 基础用法

// 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 url={req.url} />);

  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>SSR App</title></head>
      <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

客户端水合(Hydration)

服务端输出的只是静态 HTML,需要在客户端进行水合以激活事件绑定和交互逻辑:

// client.js
import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(document.getElementById('root'), <App url={window.location.pathname} />);

hydrateRootcreateRoot 的区别:

  • createRoot:从零开始创建 DOM
  • hydrateRoot:复用服务端已有的 DOM 节点,仅附加事件监听器和状态管理

如果服务端和客户端渲染结果不一致,React 会发出水合不匹配(hydration mismatch)警告,并尝试修复。

三、React 18 流式渲染

React 18 引入了 renderToPipeableStream,支持流式 SSR,配合 Suspense 实现渐进式页面加载:

import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('*', (req, res) => {
  let didError = false;

  const { pipe, abort } = renderToPipeableStream(<App url={req.url} />, {
    bootstrapScripts: ['/client.js'],

    onShellReady() {
      // Shell(非 Suspense 包裹的部分)已就绪
      res.statusCode = didError ? 500 : 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },

    onShellError(error) {
      res.statusCode = 500;
      res.send('<h1>服务器错误</h1>');
    },

    onError(error) {
      didError = true;
      console.error(error);
    },
  });

  setTimeout(() => abort(), 10000);
});

配合 Suspense 实现选择性水合

function App() {
  return (
    <html>
      <body>
        <Header />
        <Suspense fallback={<Spinner />}>
          <MainContent />
        </Suspense>
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
        </Suspense>
      </body>
    </html>
  );
}

流式渲染的工作流程:

  1. 服务端首先发送 Shell 部分(Header + 两个 Suspense fallback)
  2. MainContent 数据就绪,服务端流式推送其 HTML 并替换 fallback
  3. Comments 同理,独立于 MainContent 完成
  4. 客户端进行选择性水合——用户交互的区域优先水合

这解决了传统 SSR 的 "全部等待" 问题:不需要所有数据都就绪才能开始发送 HTML。

四、SSR 的优势与挑战

优势

  • SEO 友好:搜索引擎直接获取完整 HTML 内容
  • 更快的 FCP:用户在 JS 加载前就能看到页面内容
  • 可访问性:即使 JS 加载失败,核心内容依然可见
  • 社交媒体分享:Open Graph 等 meta 标签在分享时可被正确解析

挑战与注意事项

// 常见问题:服务端没有 window/document
function Component() {
  // 错误:服务端渲染时报错
  // const width = window.innerWidth;

  // 正确:使用 useEffect 确保只在客户端执行
  const [width, setWidth] = useState(0);

  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);

  return <div>Width: {width}</div>;
}

主要挑战:

  • 服务器负载增加:每个请求都需要渲染,CPU 密集
  • 开发复杂度:需要处理服务端/客户端环境差异
  • TTFB 可能增加:服务端渲染需要时间,首字节到达时间变长
  • 状态同步:服务端获取的数据需要传递到客户端避免重复请求
  • 第三方库兼容性:部分库依赖浏览器 API,在服务端无法使用

数据获取与状态同步

// 服务端获取数据并注入到 HTML
app.get('/user/:id', async (req, res) => {
  const userData = await fetchUser(req.params.id);

  const html = renderToString(<UserPage data={userData} />);

  res.send(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify(userData)};
        </script>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});

// 客户端复用初始数据
hydrateRoot(
  document.getElementById('root'),
  <UserPage data={window.__INITIAL_DATA__} />
);

五、Next.js 中的 SSR

Next.js 是 React 官方推荐的 SSR 框架,极大简化了服务端渲染的配置:

App Router(Next.js 13+)

// app/users/page.tsx — 默认就是服务端组件
async function UsersPage() {
  const users = await fetch('https://api.example.com/users').then(r => r.json());

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

export default UsersPage;
// app/users/[id]/page.tsx — 动态路由
async function UserDetail({ params }) {
  const user = await fetch(`https://api.example.com/users/${params.id}`).then(r => r.json());

  return <UserProfile user={user} />;
}

export default UserDetail;

客户端组件

'use client';

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数: {count}
    </button>
  );
}

export default Counter;

六、SSG、SSR、ISR 对比

渲染策略执行时机适用场景示例
SSG(静态生成)构建时内容不常变化的页面博客、文档
SSR(服务端渲染)每次请求时个性化或实时数据页面用户主页、搜索结果
ISR(增量静态再生)构建时 + 按需重新生成内容偶尔更新的页面电商商品页
// Next.js App Router 中的缓存策略
// SSG: 默认行为,fetch 自动缓存
async function StaticPage() {
  const data = await fetch('https://api.example.com/posts');
  return <PostList data={data} />;
}

// SSR: 禁用缓存
async function DynamicPage() {
  const data = await fetch('https://api.example.com/posts', {
    cache: 'no-store',
  });
  return <PostList data={data} />;
}

// ISR: 定时重新验证
async function ISRPage() {
  const data = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }, // 60 秒后重新生成
  });
  return <PostList data={data} />;
}

七、React Server Components

React Server Components(RSC)是 React 18 引入的全新架构,与 SSR 互补但不等同:

特性SSRRSC
执行环境服务端渲染 → 客户端水合仅在服务端执行,不发送到客户端
JS Bundle组件代码包含在客户端 bundle组件代码不进入客户端 bundle
交互性水合后支持交互不支持交互,需配合客户端组件
// 服务端组件:直接访问数据库,零客户端 JS
async function ArticleList() {
  const articles = await db.query('SELECT * FROM articles ORDER BY created_at DESC');

  return (
    <div>
      {articles.map((article) => (
        <article key={article.id}>
          <h2>{article.title}</h2>
          <p>{article.summary}</p>
          <LikeButton articleId={article.id} /> {/* 客户端组件 */}
        </article>
      ))}
    </div>
  );
}

RSC 让服务端组件的 JS 完全不进入客户端打包产物,显著减少了 bundle 体积。

八、总结

SSR 的核心价值在于改善首屏体验和 SEO,但也带来了架构复杂度。现代 React SSR 的演进方向:

  • React 18 流式渲染:解决了传统 SSR 的 "全有或全无" 问题
  • 选择性水合:优先水合用户正在交互的部分
  • RSC:进一步减少客户端 JS 体积
  • Next.js App Router:将 SSR、SSG、ISR、RSC 统一到一个直觉化的开发模型中

选型建议:

  • 纯内部管理后台 → CSR 即可
  • 需要 SEO 的内容型网站 → SSG 或 ISR
  • 需要实时数据 + SEO → SSR
  • 复杂应用追求极致性能 → RSC + 流式 SSR