React 服务端渲染 (SSR) 深度解析

困难 🔴React 生态
4 个标签
预计阅读时间:12 分钟
ReactSSRNext.js性能

React 服务端渲染 (SSR) 深度解析

服务端渲染 (SSR) 是提高 React 应用首屏性能和 SEO 的重要技术。与客户端渲染 (CSR) 不同,SSR 在服务器端生成完整的 HTML,然后发送给客户端,用户可以更快地看到页面内容。SSR 对于内容驱动的网站、电商、博客等需要良好 SEO 的应用尤为重要。

SSR 核心概念

SSR 工作原理:

服务端渲染的工作流程包括:服务器接收请求、执行 React 组件渲染、生成完整 HTML、发送给客户端、客户端进行 hydration(激活交互)。Hydration 是 SSR 的关键步骤,React 在客户端重新执行组件,绑定事件处理器,使页面变得可交互。Hydration 过程中,React 会对比服务端渲染的 HTML 和客户端渲染的结果,确保一致性。

SSR 的优势:

更快的首屏加载,用户可以更快看到页面内容;更好的 SEO,搜索引擎可以抓取完整的页面内容;支持社交媒体分享,Open Graph 和 Twitter Card 标签可以正确显示;更好的可访问性,屏幕阅读器可以读取完整的页面内容。

SSR 的挑战:

服务器负载增加,每次请求都需要渲染;开发复杂度提高,需要处理服务器和客户端环境的差异;某些浏览器 API 不可用,如 window、document;hydration 错误需要特别注意,服务端和客户端渲染结果必须一致。

代码示例

javascriptCode
// Next.js App Router - 服务器组件
// app/users/page.tsx
async function UsersPage() {
  // 直接在服务器组件中获取数据
  const users = await fetch('https://api.example.com/users', {
    cache: 'no-store' // 每次请求都重新获取
  }).then(res => res.json());
  
  return (
    <main>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </main>
  );
}

// 静态生成 (SSG)
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  
  return posts.map(post => ({
    slug: post.slug
  }));
}

async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 } // ISR: 每小时重新验证
  }).then(res => res.json());
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Pages Router - getServerSideProps
export async function getServerSideProps(context) {
  const { params, req, res } = context;
  
  // 设置缓存头
  res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59');
  
  const data = await fetch(`https://api.example.com/data/${params.id}`);
  const item = await data.json();
  
  return {
    props: { item }
  };
}

function ServerPage({ item }) {
  return <div>{item.name}</div>;
}

// Pages Router - getStaticProps
export async function getStaticProps() {
  const data = await fetch('https://api.example.com/posts');
  const posts = await data.json();
  
  return {
    props: { posts },
    revalidate: 60 // ISR: 60秒后重新生成
  };
}

// Pages Router - getStaticPaths
export async function getStaticPaths() {
  const data = await fetch('https://api.example.com/posts');
  const posts = await data.json();
  
  const paths = posts.map(post => ({
    params: { id: post.id.toString() }
  }));
  
  return {
    paths,
    fallback: 'blocking' // 或 true, false
  };
}

// 自定义 SSR 服务器
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './App';

const app = express();

app.get('*', (req, res) => {
  const context = {};
  
  const html = renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );
  
  if (context.url) {
    res.redirect(context.url);
    return;
  }
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});

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

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

// 流式 SSR
import { renderToPipeableStream } from 'react-dom/server';

app.get('*', (req, res) => {
  const { pipe } = renderToPipeableStream(
    <App />,
    {
      bootstrapScripts: ['/client.js'],
      onShellReady() {
        res.setHeader('Content-Type', 'text/html');
        pipe(res);
      },
      onShellError(error) {
        res.status(500).send('Error');
      }
    }
  );
});

// 客户端组件与 SSR 配合
'use client';

import { useState, useEffect } from 'react';

function ClientComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // 只在客户端执行
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);
  
  if (!data) return <div>Loading...</div>;
  
  return <div>{data.message}</div>;
}

// 服务器组件中嵌入客户端组件
async function ServerComponent() {
  const initialData = await fetchInitialData();
  
  return (
    <div>
      <h1>Server Rendered</h1>
      <ClientComponent initialData={initialData} />
    </div>
  );
}

// 处理 SSR 中的环境差异
function SafeComponent() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return (
    <div>
      {isClient ? (
        <ClientOnlyFeature />
      ) : (
        <ServerFallback />
      )}
    </div>
  );
}

// 动态导入避免 SSR
const DynamicComponent = dynamic(() => import('./ClientOnlyComponent'), {
  ssr: false,
  loading: () => <p>Loading...</p>
});

Next.js 渲染模式详解

静态生成 (SSG):

静态生成在构建时生成 HTML 文件,适合内容不经常变化的页面。SSG 提供了最佳的性能,因为 HTML 文件可以直接由 CDN 分发。使用 getStaticProps 获取数据,使用 getStaticPaths 生成动态路由。SSG 适合博客、文档、产品列表等场景。

服务端渲染 (SSR):

服务端渲染在每次请求时生成 HTML,适合内容经常变化的页面。SSR 提供了良好的 SEO 和首屏性能,但会增加服务器负载。使用 getServerSideProps 获取数据。SSR 适合新闻、电商、社交媒体等场景。

增量静态再生 (ISR):

ISR 结合了 SSG 和 SSR 的优点,初始时使用静态页面,当数据变化时重新生成特定页面。ISR 通过 revalidate 属性设置重新生成的时间间隔。ISR 适合内容定期更新的场景,如博客、新闻、电商等。

性能优化策略

代码分割:

使用动态导入 (dynamic import) 按需加载组件,减少初始加载体积。Next.js 自动按路由分割代码,也可以手动分割大型组件。

缓存策略:

合理设置 HTTP 缓存头,使用 CDN 缓存静态资源。对于 ISR 页面,设置合适的 revalidate 时间。使用 Cache-Control 头控制浏览器缓存行为。

流式渲染:

使用 renderToPipeableStream 或 renderToReadableStream 实现流式 SSR,逐步发送 HTML 到客户端,用户可以更快看到内容。配合 Suspense 实现组件级别的流式渲染。

预加载和预取:

使用 预加载关键资源。Next.js 的 组件自动预取链接页面。使用 router.prefetch() 手动预取页面。