第 5 章:React Server Components 深入解析

深入理解 React Server Components 的内部机制:设计动机、与传统 SSR 的本质差异、Hybrid Rendering、多层缓存系统、流式渲染流水线,以及 RSC 的限制与最佳实践。

本章是 RSC 的深度解析章节。读完本章,你将掌握 React Server Components 的内部工作原理,理解多层缓存系统,并能够在生产环境中正确使用 RSC。


5.1 Server Components 的设计动机

传统 React 应用的痛点

在理解 RSC 之前,先看看传统 React 应用面临的问题。

痛点 1:数据获取的复杂性

传统方式(CSR)

// 客户端组件
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const res = await fetch(`/api/products/${productId}`);
        const data = await res.json();
        setProduct(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }
    fetchData();
  }, [productId]);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  if (!product) return <NotFound />;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

问题

  • 需要手动管理 loadingerrordata 状态
  • 需要处理竞态条件(Race Condition)
  • 需要处理组件卸载时的清理
  • 需要处理缓存、重试、刷新逻辑

解决方案:使用 SWR、React Query 等库,但这又引入了新的复杂性。

痛点 2:Waterfall 请求

问题:嵌套组件导致串行请求。

function App() {
  return <Layout />;
}

function Layout() {
  const user = useUser(); // 请求 1:获取用户
  return (
    <div>
      <Header user={user} />
      <Sidebar />
      <Main />
    </div>
  );
}

function Sidebar() {
  const categories = useCategories(); // 请求 2:获取分类(等待请求 1)
  return <nav>{/* ... */}</nav>;
}

function Main() {
  const products = useProducts(); // 请求 3:获取商品(等待请求 1、2)
  return <div>{/* ... */}</div>;
}

时间线

0ms    请求 1:获取用户
200ms  请求 1 完成
200ms  请求 2:获取分类
400ms  请求 2 完成
400ms  请求 3:获取商品
600ms  请求 3 完成
600ms  页面渲染完成

问题:总耗时 = 200ms × 3 = 600ms(串行)。

痛点 3:客户端 JavaScript 体积

问题:所有组件代码都打包到客户端。

import ReactMarkdown from 'react-markdown'; // 100KB
import { format } from 'date-fns'; // 50KB
import { db } from '@/lib/database'; // 包含数据库驱动

function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{format(post.date, 'yyyy-MM-dd')}</p>
      <ReactMarkdown>{post.content}</ReactMarkdown>
    </article>
  );
}

问题

  • ReactMarkdown(100KB)发送到客户端,但只在服务端需要
  • date-fns(50KB)发送到客户端,但只在服务端需要
  • 数据库驱动(敏感)不能发送到客户端

RSC 的解决方案

React Server Components 的设计目标:

  1. 简化数据获取:直接在组件中使用 async/await
  2. 消除 Waterfall:服务端并行获取数据
  3. 减少客户端 JS:服务端组件代码不发送到客户端
  4. 直接访问后端资源:数据库、文件系统、内部 API

解决方案 1:简化数据获取

RSC 方式

// Server Component
async function ProductPage({ productId }) {
  const product = await db.query(
    'SELECT * FROM products WHERE id = ?',
    [productId]
  );

  if (!product) {
    return <NotFound />;
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

优势

  • ✅ 无需 useStateuseEffect
  • ✅ 无需手动管理 loadingerror 状态
  • ✅ 代码更简洁、更易读
  • ✅ 直接访问数据库,无需 API 层

解决方案 2:消除 Waterfall

RSC 并行获取

function App() {
  return <Layout />;
}

async function Layout() {
  // 并行获取数据
  const [user, categories, products] = await Promise.all([
    getUser(),
    getCategories(),
    getProducts(),
  ]);

  return (
    <div>
      <Header user={user} />
      <Sidebar categories={categories} />
      <Main products={products} />
    </div>
  );
}

时间线

0ms    并行请求:用户、分类、商品
200ms  所有请求完成
200ms  页面渲染完成

结果:总耗时 = 200ms(并行),比串行快 3 倍。

解决方案 3:减少客户端 JS

RSC 自动拆分

// Server Component(代码不发送到客户端)
import ReactMarkdown from 'react-markdown'; // 100KB
import { format } from 'date-fns'; // 50KB

async function BlogPost({ postId }) {
  const post = await db.query('SELECT * FROM posts WHERE id = ?', [postId]);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{format(post.date, 'yyyy-MM-dd')}</p>
      <ReactMarkdown>{post.content}</ReactMarkdown>
    </article>
  );
}

结果

  • ReactMarkdown(100KB)在服务端执行,不发送到客户端
  • date-fns(50KB)在服务端执行,不发送到客户端
  • 客户端只接收 RSC Payload(约 5KB)

客户端 JS 体积:从 150KB 减少到 5KB(减少 97%)。

解决方案 4:直接访问后端资源

直接访问数据库

async function UsersPage() {
  // 直接查询数据库,无需 API
  const users = await db.query('SELECT * FROM users');
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

直接读取文件系统

import { readFile } from 'fs/promises';

async function DocsPage() {
  // 直接读取 Markdown 文件
  const content = await readFile('./docs/intro.md', 'utf-8');
  
  return <ReactMarkdown>{content}</ReactMarkdown>;
}

直接调用内部 API

async function AnalyticsPage() {
  // 直接调用内部微服务
  const stats = await internalAPI.get('/analytics/stats');
  
  return <Dashboard stats={stats} />;
}

5.2 RSC vs 传统 SSR:性能与架构的本质差异

传统 SSR 的工作流程

步骤 1:服务端渲染 HTML

// 服务端
import { renderToString } from 'react-dom/server';

app.get('/', async (req, res) => {
  const html = renderToString(<App />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

生成的 HTML

<div id="root">
  <header>
    <h1>My App</h1>
  </header>
  <main>
    <p>Welcome!</p>
  </main>
</div>

步骤 2:客户端 Hydration

// 客户端
import { hydrateRoot } from 'react-dom/client';
import App from './App';

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

Hydration 的作用

  1. React 接管服务端生成的 HTML
  2. 重新执行所有组件代码
  3. 添加事件监听器
  4. 建立状态管理

问题

  • 需要下载完整的 JS Bundle(包含所有组件代码)
  • 需要重新执行所有组件(浪费 CPU)
  • Hydration 期间页面不可交互

RSC 的工作流程

步骤 1:服务端执行组件

// Server Component
async function BlogPost({ postId }) {
  const post = await db.query('SELECT * FROM posts WHERE id = ?', [postId]);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

服务端执行

  1. 执行组件函数
  2. 生成 RSC Payload(不是 HTML)

步骤 2:生成 RSC Payload

RSC Payload 示例

{
  "type": "article",
  "props": {
    "children": [
      {
        "type": "h1",
        "props": { "children": "Hello World" }
      },
      {
        "type": "p",
        "props": { "children": "This is a blog post." }
      }
    ]
  }
}

特点

  • 是组件树的序列化表示
  • 不包含 JavaScript 代码
  • 体积小(约 1-5KB)

步骤 3:客户端渲染

浏览器接收 RSC Payload

  1. 解析 RSC Payload
  2. 直接渲染为 DOM
  3. 无需 Hydration

核心差异对比

维度传统 SSRRSC
服务端输出HTML 字符串RSC Payload(JSON)
客户端 JS完整 Bundle(100-500KB)轻量 Runtime(10KB)
Hydration✅ 需要(重新执行所有组件)❌ 不需要
可交互Hydration 后可交互需要 Client Component
组件代码发送到客户端不发送到客户端
性能中等(Hydration 成本高)极好(零 Hydration)

性能对比

场景:博客文章页面

传统 SSR

0ms     服务端渲染 HTML
50ms    返回 HTML(50KB)
100ms   浏览器显示 HTML
200ms   下载 JS Bundle(200KB)
400ms   Hydration 开始
600ms   Hydration 完成(可交互)

RSC

0ms     服务端执行组件
30ms    生成 RSC Payload(5KB)
50ms    返回 RSC Payload
80ms    浏览器渲染 DOM(完成)

结果

  • SSR:600ms 可交互
  • RSC:80ms 完成渲染
  • RSC 快 7.5 倍

场景:包含大量组件的页面

页面包含

  • ReactMarkdown(100KB)
  • date-fns(50KB)
  • 图表库(150KB)
  • 自定义组件(100KB)

传统 SSR

JS Bundle 大小:400KB
Hydration 时间:800ms
总加载时间:1200ms

RSC(只有图表需要交互):

Client Component JS:150KB(图表库)
RSC Payload:10KB
Hydration 时间:200ms(只 Hydration 图表)
总加载时间:400ms

结果

  • JS 体积减少 62%
  • 加载时间减少 67%

架构差异

传统 SSR 架构

┌─────────────────────────────────────────┐
│              服务端                      │
│  ┌─────────────────────────────────┐   │
│  │  renderToString(<App />)        │   │
│  │  ↓                              │   │
│  │  HTML 字符串                     │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│              客户端                      │
│  ┌─────────────────────────────────┐   │
│  │  下载 JS Bundle(400KB)         │   │
│  │  ↓                              │   │
│  │  hydrateRoot(<App />)           │   │
│  │  ↓                              │   │
│  │  重新执行所有组件(浪费 CPU)     │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

问题

  • 服务端和客户端执行相同的代码(重复工作)
  • 客户端需要下载完整 JS Bundle
  • Hydration 成本高

RSC 架构

┌─────────────────────────────────────────┐
│              服务端                      │
│  ┌─────────────────────────────────┐   │
│  │  Server Components              │   │
│  │  - 执行组件逻辑                  │   │
│  │  - 访问数据库                    │   │
│  │  - 生成 RSC Payload             │   │
│  └─────────────────────────────────┘   │
│  ┌─────────────────────────────────┐   │
│  │  Client Components              │   │
│  │  - 打包到客户端 JS               │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│              客户端                      │
│  ┌─────────────────────────────────┐   │
│  │  下载 RSC Payload(5KB)         │   │
│  │  ↓                              │   │
│  │  渲染 DOM(无需 Hydration)      │   │
│  └─────────────────────────────────┘   │
│  ┌─────────────────────────────────┐   │
│  │  下载 Client Component JS       │   │
│  │  ↓                              │   │
│  │  Hydration(只交互组件)         │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

优势

  • 服务端组件代码不发送到客户端
  • 客户端只 Hydration 需要交互的组件
  • 减少重复工作

5.3 Hybrid Rendering:服务端组件嵌入客户端交互

什么是 Hybrid Rendering

Hybrid Rendering(混合渲染):在同一页面中混合使用 Server Components 和 Client Components。

核心思想

  • Server Components:处理数据获取、展示型内容
  • Client Components:处理交互、状态管理、浏览器 API

组合模式

模式 1:Server Component 包含 Client Component

最常见模式:父组件是 Server Component,子组件是 Client Component。

// app/page.tsx(Server Component)
async function HomePage() {
  const posts = await db.query('SELECT * FROM posts');

  return (
    <div>
      <h1>Blog</h1>
      <PostList posts={posts} />
      <LikeButton /> {/* Client Component */}
    </div>
  );
}

export default HomePage;
// app/LikeButton.tsx(Client Component)
"use client";

import { useState } from 'react';

export function LikeButton() {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(l => l + 1)}>
      Likes: {likes}
    </button>
  );
}

渲染流程

  1. 服务端执行 HomePage,获取 posts
  2. 生成 RSC Payload,包含 PostListLikeButton 的占位符
  3. 客户端渲染 DOM
  4. 下载 LikeButton 的 JS
  5. Hydration LikeButton

模式 2:Client Component 包含 Server Component(通过 children)

场景:交互式容器包含展示型内容。

// app/page.tsx(Server Component)
import { Modal } from './Modal';
import { PostContent } from './PostContent';

async function HomePage() {
  return (
    <Modal>
      <PostContent /> {/* Server Component */}
    </Modal>
  );
}

export default HomePage;
// app/Modal.tsx(Client Component)
"use client";

import { useState } from 'react';

export function Modal({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      {isOpen && (
        <div className="modal">
          {children}
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </div>
  );
}
// app/PostContent.tsx(Server Component)
async function PostContent() {
  const post = await db.query('SELECT * FROM posts LIMIT 1');

  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
    </article>
  );
}

关键点

  • Modal 是 Client Component(需要交互)
  • PostContent 是 Server Component(通过 children 传入)
  • PostContent 在服务端执行,不发送 JS 到客户端

模式 3:交替嵌套(Interleaving)

场景:Server Component → Client Component → Server Component。

// app/page.tsx(Server Component)
async function HomePage() {
  const user = await getUser();

  return (
    <Layout>
      <UserProfile user={user} />
    </Layout>
  );
}
// app/Layout.tsx(Client Component)
"use client";

import { useState } from 'react';

export function Layout({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light');

  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      {children}
    </div>
  );
}
// app/UserProfile.tsx(Server Component)
async function UserProfile({ user }) {
  const posts = await db.query(
    'SELECT * FROM posts WHERE author_id = ?',
    [user.id]
  );

  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

渲染流程

服务端:
1. 执行 HomePage(Server Component)
2. 执行 UserProfile(Server Component)
3. 生成 RSC Payload

客户端:
1. 渲染 DOM
2. 下载 Layout 的 JS(Client Component)
3. Hydration Layout

最佳实践

1. 将 Client Component 下沉到叶子节点

原则:尽量减少 Client Component 的范围。

// ❌ 不好的做法:整个页面都是 Client Component
"use client";

export default function Page() {
  return (
    <div>
      <Header />
      <ProductList />
      <Footer />
      <LikeButton /> {/* 只有这个需要交互 */}
    </div>
  );
}

// ✅ 好的做法:只有 LikeButton 是 Client Component
export default function Page() {
  return (
    <div>
      <Header />
      <ProductList />
      <Footer />
      <LikeButton /> {/* "use client" */}
    </div>
  );
}

优势

  • 减少客户端 JS 体积
  • 更多组件在服务端执行(更快)
  • 更好的 SEO

2. 传递序列化数据

原则:Server Component 传递给 Client Component 的 props 必须是可序列化的。

// ✅ 可序列化的 props
<ClientComponent
  title="Hello"           // string
  count={42}              // number
  isActive={true}         // boolean
  items={[1, 2, 3]}       // array
  user={{ name: 'Alice' }} // object
/>

// ❌ 不可序列化的 props
<ClientComponent
  onClick={() => {}}      // function
  date={new Date()}       // Date 对象(需要转换)
  regex={/test/g}         // RegExp
  component={<MyComp />}  // React Element(除非是 children)
/>

3. 使用 children 传递 Server Component

场景:Client Component 需要包含 Server Component。

// ✅ 好的做法:通过 children 传递
<Modal> {/* Client Component */}
  <PostContent /> {/* Server Component */}
</Modal>

// ❌ 不好的做法:在 Client Component 中导入 Server Component
"use client";

import { PostContent } from './PostContent'; // ❌ 会变成 Client Component

export function Modal() {
  return <PostContent />;
}

5.4 Server Components 的多层缓存机制

Next.js 为 RSC 提供了 4 层缓存系统,优化性能和用户体验。

缓存架构概览

┌─────────────────────────────────────────────────┐
│  第 1 层:Request Memoization(请求级去重)       │
│  - 作用域:单次请求                              │
│  - 目的:避免重复 fetch 相同数据                  │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│  第 2 层:Data Cache(数据缓存)                  │
│  - 作用域:跨请求持久化                          │
│  - 目的:缓存 fetch 结果,减少 API 调用           │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│  第 3 层:Full Route Cache(整页缓存)            │
│  - 作用域:跨请求持久化                          │
│  - 目的:缓存整个页面的 RSC Payload               │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│  第 4 层:Router Cache(路由缓存)                │
│  - 作用域:客户端浏览器                          │
│  - 目的:缓存已访问页面的 RSC Payload             │
└─────────────────────────────────────────────────┘

第 1 层:Request Memoization(请求级去重)

作用:在单次请求中,自动去重相同的 fetch 调用。

场景:多个组件请求相同的数据。

// app/layout.tsx
async function Layout() {
  const user = await fetch('https://api.example.com/user').then(r => r.json());
  
  return (
    <div>
      <Header user={user} />
      <Sidebar />
      <Main />
    </div>
  );
}
// app/Header.tsx
async function Header() {
  // 相同的 fetch 调用
  const user = await fetch('https://api.example.com/user').then(r => r.json());
  
  return <header>Welcome, {user.name}!</header>;
}
// app/Sidebar.tsx
async function Sidebar() {
  // 相同的 fetch 调用
  const user = await fetch('https://api.example.com/user').then(r => r.json());
  
  return <aside>Profile: {user.name}</aside>;
}

行为

请求 1:Layout 调用 fetch('/user')
Next.js 缓存结果
请求 2:Header 调用 fetch('/user')
Next.js 返回缓存结果(不发起网络请求)
请求 3:Sidebar 调用 fetch('/user')
Next.js 返回缓存结果(不发起网络请求)

结果:只发起 1 次网络请求,而不是 3 次。

作用域:单次请求(请求完成后缓存清除)。

手动控制

// 跳过缓存
const data = await fetch('https://api.example.com/user', {
  cache: 'no-store',
});

// 使用 AbortController 取消请求
const controller = new AbortController();
const data = await fetch('https://api.example.com/user', {
  signal: controller.signal,
});

第 2 层:Data Cache(数据缓存)

作用:跨请求持久化缓存 fetch 结果。

场景:博客文章列表,每小时更新一次。

// app/blog/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // 缓存 1 小时
  });
  return res.json();
}

export default async function BlogPage() {
  const posts = await getPosts();
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

行为

首次请求:
1. fetch('/posts') → 网络请求
2. 缓存结果到 Data Cache
3. 返回数据

后续请求(1 小时内):
1. fetch('/posts') → 检查 Data Cache
2. 缓存命中 → 返回缓存数据(无网络请求)

1 小时后:
1. fetch('/posts') → 检查 Data Cache
2. 缓存过期 → 重新发起网络请求
3. 更新缓存

缓存选项

// 静态缓存(永久,直到重新部署)
fetch('https://api.example.com/posts', {
  cache: 'force-cache', // 默认行为
});

// 定时缓存(ISR)
fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }, // 1 小时
});

// 不缓存(动态)
fetch('https://api.example.com/posts', {
  cache: 'no-store',
});

按需失效

import { revalidatePath, revalidateTag } from 'next/cache';

// 在 Server Action 中
async function createPost(formData: FormData) {
  "use server";
  
  await db.query('INSERT INTO posts ...');
  
  // 失效博客列表缓存
  revalidatePath('/blog');
}

// 使用 Tags
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['blog-posts'] },
  });
  return res.json();
}

async function createPost(formData: FormData) {
  "use server";
  
  await db.query('INSERT INTO posts ...');
  
  // 失效所有带 'blog-posts' 标签的缓存
  revalidateTag('blog-posts');
}

第 3 层:Full Route Cache(整页缓存)

作用:缓存整个页面的 RSC Payload 和 HTML。

场景:静态页面,构建时生成。

// app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>We are a company.</p>
    </div>
  );
}

行为

构建时:
1. 渲染 AboutPage
2. 生成 RSC Payload
3. 生成 HTML
4. 缓存到 Full Route Cache

请求时:
1. 检查 Full Route Cache
2. 缓存命中 → 直接返回缓存的 RSC Payload 和 HTML
3. 无需重新渲染

自动缓存条件

  • 页面不使用动态 API(cookies()headers()searchParams
  • 所有 fetch 都使用静态缓存

强制动态渲染

// 禁用 Full Route Cache
export const dynamic = 'force-dynamic';

export default function Page() {
  // 每次请求都重新渲染
  return <div>{new Date().toISOString()}</div>;
}

按路径失效

import { revalidatePath } from 'next/cache';

// 失效单个页面
revalidatePath('/about');

// 失效整个目录
revalidatePath('/blog');

// 失效所有页面
revalidatePath('/', 'layout');

第 4 层:Router Cache(路由缓存)

作用:在客户端浏览器中缓存已访问页面的 RSC Payload。

场景:用户在页面之间导航。

// app/page.tsx
import Link from 'next/link';

export default function HomePage() {
  return (
    <div>
      <h1>Home</h1>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </div>
  );
}

行为

1. 用户访问 /home
   → 下载 /home 的 RSC Payload
   → 缓存到 Router Cache

2. 用户点击 "About"
   → 导航到 /about
   → 下载 /about 的 RSC Payload
   → 缓存到 Router Cache

3. 用户点击浏览器"后退"
   → 返回 /home
   → 检查 Router Cache
   → 缓存命中 → 立即显示(无网络请求)

4. 用户点击"前进"
   → 返回 /about
   → 检查 Router Cache
   → 缓存命中 → 立即显示(无网络请求)

优势

  • ✅ 即时导航(无加载时间)
  • ✅ 保留滚动位置
  • ✅ 减少服务器负载

缓存持续时间

  • 默认:5 分钟(静态页面)
  • 动态页面:不缓存

手动控制

// 在 next.config.js 中配置
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30, // 动态页面缓存 30 秒
    },
  },
};

使用 router.refresh() 清除缓存

"use client";

import { useRouter } from 'next/navigation';

export function RefreshButton() {
  const router = useRouter();

  return (
    <button onClick={() => router.refresh()}>
      Refresh
    </button>
  );
}

缓存层级协作

完整请求流程

用户访问 /blog
1. 检查 Router Cache(客户端)
   ├─ 命中 → 立即显示(结束)
   └─ 未命中 → 发起网络请求
2. 检查 Full Route Cache(服务端)
   ├─ 命中 → 返回缓存的 RSC Payload(结束)
   └─ 未命中 → 渲染页面
3. 渲染页面
4. 执行 Server Components
5. 调用 fetch()
6. 检查 Request Memoization
   ├─ 命中 → 返回缓存结果
   └─ 未命中 → 检查 Data Cache
7. 检查 Data Cache
   ├─ 命中 → 返回缓存数据
   └─ 未命中 → 发起网络请求
8. 缓存结果到 Data Cache
9. 缓存 RSC Payload 到 Full Route Cache
10. 返回 RSC Payload 到客户端
11. 缓存到 Router Cache

缓存策略最佳实践

1. 静态内容:使用 Full Route Cache

// 静态页面(构建时缓存)
export default function AboutPage() {
  return <div>About Us</div>;
}

2. 半动态内容:使用 Data Cache + ISR

// 博客列表(每小时更新)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 },
  });
  return res.json();
}

3. 动态内容:禁用缓存

// 用户 Dashboard(每次请求都不同)
export const dynamic = 'force-dynamic';

async function getDashboard() {
  const res = await fetch('https://api.example.com/dashboard', {
    cache: 'no-store',
  });
  return res.json();
}

4. 共享数据:使用 Tags

// 多个页面共享的数据
async function getUser() {
  const res = await fetch('https://api.example.com/user', {
    next: { tags: ['user-data'] },
  });
  return res.json();
}

// 更新时失效所有相关页面
async function updateUser(formData: FormData) {
  "use server";
  
  await db.query('UPDATE users ...');
  revalidateTag('user-data');
}

5.5 流式渲染流水线:Suspense + Streaming 的完整链路

什么是流式渲染

流式渲染(Streaming):服务端分段发送 HTML,客户端渐进渲染。

传统渲染

服务端:渲染整个页面 → 发送完整 HTML
客户端:等待完整 HTML → 显示页面

流式渲染

服务端:渲染第 1 部分 → 发送
       渲染第 2 部分 → 发送
       渲染第 3 部分 → 发送
客户端:接收第 1 部分 → 显示
       接收第 2 部分 → 显示
       接收第 3 部分 → 显示

流式渲染的工作原理

步骤 1:服务端分段渲染

// app/page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header />
      
      <Suspense fallback={<div>Loading main...</div>}>
        <MainContent />
      </Suspense>
      
      <Suspense fallback={<div>Loading sidebar...</div>}>
        <Sidebar />
      </Suspense>
    </div>
  );
}

服务端执行

  1. 渲染 <Header />(立即完成)
  2. 渲染 <Suspense>(显示 fallback)
  3. 渲染 <MainContent />(异步,等待数据)
  4. 渲染 <Sidebar />(异步,等待数据)

步骤 2:分段发送 HTML

第 1 段(立即发送)

<!DOCTYPE html>
<html>
<body>
  <header>
    <h1>My App</h1>
  </header>
  
  <div>Loading main...</div>
  <div>Loading sidebar...</div>
  
  <script>
    // Next.js Streaming Runtime
  </script>
</body>
</html>

浏览器立即显示

┌─────────────────────────┐
│  My App                 │
├─────────────────────────┤
│  Loading main...        │
│  Loading sidebar...     │
└─────────────────────────┘

第 2 段(2 秒后,MainContent 完成)

<script>
  // 替换 "Loading main..." 为真实内容
  document.querySelector('[data-suspense="main"]').innerHTML = `
    <main>
      <h2>Main Content</h2>
      <p>This is the main content.</p>
    </main>
  `;
</script>

浏览器更新

┌─────────────────────────┐
│  My App                 │
├─────────────────────────┤
│  Main Content           │
│  This is the main...    │
│                         │
│  Loading sidebar...     │
└─────────────────────────┘

第 3 段(4 秒后,Sidebar 完成)

<script>
  // 替换 "Loading sidebar..." 为真实内容
  document.querySelector('[data-suspense="sidebar"]').innerHTML = `
    <aside>
      <h3>Sidebar</h3>
      <ul>
        <li>Link 1</li>
        <li>Link 2</li>
      </ul>
    </aside>
  `;
</script>

浏览器最终状态

┌─────────────────────────┐
│  My App                 │
├─────────────────────────┤
│  Main Content           │
│  This is the main...    │
│                         │
│  Sidebar                │
│  - Link 1               │
│  - Link 2               │
└─────────────────────────┘

实战:流式渲染 Dashboard

// app/dashboard/page.tsx
import { Suspense } from 'react';
import AnalyticsCard from './AnalyticsCard';
import RevenueCard from './RevenueCard';
import UsersCard from './UsersCard';
import OrdersCard from './OrdersCard';
import CardSkeleton from './CardSkeleton';

export default function DashboardPage() {
  return (
    <div className="p-8">
      <h1 className="text-3xl font-bold mb-8">Dashboard</h1>
      
      <div className="grid grid-cols-2 gap-4">
        <Suspense fallback={<CardSkeleton />}>
          <AnalyticsCard />
        </Suspense>
        
        <Suspense fallback={<CardSkeleton />}>
          <RevenueCard />
        </Suspense>
        
        <Suspense fallback={<CardSkeleton />}>
          <UsersCard />
        </Suspense>
        
        <Suspense fallback={<CardSkeleton />}>
          <OrdersCard />
        </Suspense>
      </div>
    </div>
  );
}
// app/dashboard/AnalyticsCard.tsx
async function getAnalytics() {
  // 模拟慢速 API(2 秒)
  await new Promise(resolve => setTimeout(resolve, 2000));
  return { views: 1234, visitors: 567 };
}

export default async function AnalyticsCard() {
  const analytics = await getAnalytics();
  
  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h2 className="text-xl font-bold">Analytics</h2>
      <p className="text-3xl font-bold text-blue-600">
        {analytics.views} views
      </p>
      <p className="text-gray-600">
        {analytics.visitors} visitors
      </p>
    </div>
  );
}
// app/dashboard/RevenueCard.tsx
async function getRevenue() {
  // 模拟更慢的 API(4 秒)
  await new Promise(resolve => setTimeout(resolve, 4000));
  return { total: 9876, growth: 12 };
}

export default async function RevenueCard() {
  const revenue = await getRevenue();
  
  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h2 className="text-xl font-bold">Revenue</h2>
      <p className="text-3xl font-bold text-green-600">
        ${revenue.total}
      </p>
      <p className="text-gray-600">
        +{revenue.growth}% growth
      </p>
    </div>
  );
}
// app/dashboard/UsersCard.tsx
async function getUsers() {
  // 模拟快速 API(1 秒)
  await new Promise(resolve => setTimeout(resolve, 1000));
  return { total: 890, active: 234 };
}

export default async function UsersCard() {
  const users = await getUsers();
  
  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h2 className="text-xl font-bold">Users</h2>
      <p className="text-3xl font-bold text-purple-600">
        {users.total}
      </p>
      <p className="text-gray-600">
        {users.active} active
      </p>
    </div>
  );
}
// app/dashboard/OrdersCard.tsx
async function getOrders() {
  // 模拟中等速度 API(3 秒)
  await new Promise(resolve => setTimeout(resolve, 3000));
  return { total: 456, pending: 23 };
}

export default async function OrdersCard() {
  const orders = await getOrders();
  
  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h2 className="text-xl font-bold">Orders</h2>
      <p className="text-3xl font-bold text-orange-600">
        {orders.total}
      </p>
      <p className="text-gray-600">
        {orders.pending} pending
      </p>
    </div>
  );
}
// app/dashboard/CardSkeleton.tsx
export default function CardSkeleton() {
  return (
    <div className="bg-white p-6 rounded-lg shadow animate-pulse">
      <div className="h-6 bg-gray-200 rounded w-1/3 mb-4"></div>
      <div className="h-10 bg-gray-200 rounded w-2/3 mb-2"></div>
      <div className="h-4 bg-gray-200 rounded w-1/2"></div>
    </div>
  );
}

时间线

0ms     页面加载,显示 4 个 Skeleton
1000ms  UsersCard 完成,替换第 3 个 Skeleton
2000ms  AnalyticsCard 完成,替换第 1 个 Skeleton
3000ms  OrdersCard 完成,替换第 4 个 Skeleton
4000ms  RevenueCard 完成,替换第 2 个 Skeleton

用户体验

  • 0ms:立即看到页面框架和加载状态
  • 1000ms:看到第一个数据(Users)
  • 2000ms:看到更多数据(Analytics)
  • 3000ms:看到更多数据(Orders)
  • 4000ms:看到所有数据(Revenue)

对比非流式渲染

非流式:
0ms     页面加载,显示白屏
4000ms  所有数据加载完成,显示完整页面

流式:
0ms     页面加载,显示 Skeleton
1000ms  显示部分数据
4000ms  显示所有数据

结果:流式渲染让用户更快看到内容(1 秒 vs 4 秒)。

流式渲染的最佳实践

1. 为慢速组件添加 Suspense

<Suspense fallback={<Skeleton />}>
  <SlowComponent />
</Suspense>

2. 使用 loading.tsx 自动触发

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />;
}

3. 细粒度分割

// ✅ 好的做法:每个 Card 独立 Suspense
<div className="grid grid-cols-2 gap-4">
  <Suspense fallback={<CardSkeleton />}>
    <Card1 />
  </Suspense>
  
  <Suspense fallback={<CardSkeleton />}>
    <Card2 />
  </Suspense>
</div>

// ❌ 不好的做法:整个 Grid 一个 Suspense
<Suspense fallback={<GridSkeleton />}>
  <div className="grid grid-cols-2 gap-4">
    <Card1 />
    <Card2 />
  </div>
</Suspense>

4. 使用有意义的 Fallback

// ✅ 好的做法:显示 Skeleton
<Suspense fallback={<CardSkeleton />}>
  <AnalyticsCard />
</Suspense>

// ❌ 不好的做法:显示空白
<Suspense fallback={<div />}>
  <AnalyticsCard />
</Suspense>

5.6 RSC 的限制、陷阱与最佳实践

RSC 的限制

限制 1:不能使用 Hooks

问题:Server Components 不能使用 useStateuseEffectuseContext 等。

// ❌ 错误:Server Component 使用 useState
async function Counter() {
  const [count, setCount] = useState(0); // ❌ 错误
  
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

// ✅ 正确:使用 Client Component
"use client";

function Counter() {
  const [count, setCount] = useState(0);
  
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

可用 Hooks(Server Component):

  • use():读取 Promise
  • useId():生成唯一 ID

限制 2:不能使用事件监听器

问题:Server Components 不能使用 onClickonChange 等。

// ❌ 错误:Server Component 使用 onClick
function Button() {
  return <button onClick={() => alert('Clicked!')}>Click me</button>;
}

// ✅ 正确:使用 Client Component
"use client";

function Button() {
  return <button onClick={() => alert('Clicked!')}>Click me</button>;
}

限制 3:不能使用浏览器 API

问题:Server Components 在服务端执行,无法访问 windowdocumentlocalStorage

// ❌ 错误:Server Component 使用 localStorage
function ThemeProvider() {
  const theme = localStorage.getItem('theme'); // ❌ 错误
  
  return <div className={theme}>...</div>;
}

// ✅ 正确:使用 Client Component
"use client";

import { useEffect, useState } from 'react';

function ThemeProvider() {
  const [theme, setTheme] = useState('light');
  
  useEffect(() => {
    const stored = localStorage.getItem('theme');
    if (stored) setTheme(stored);
  }, []);
  
  return <div className={theme}>...</div>;
}

限制 4:Props 必须可序列化

问题:Server Component 传递给 Client Component 的 props 必须是可序列化的。

// ❌ 错误:传递函数
<ServerComponent>
  <ClientComponent onClick={() => {}} /> {/* ❌ 函数不可序列化 */}
</ServerComponent>

// ✅ 正确:传递数据
<ServerComponent>
  <ClientComponent label="Click me" /> {/* ✅ 字符串可序列化 */}
</ServerComponent>

可序列化的类型

  • ✅ 原始类型:stringnumberbooleannullundefined
  • ✅ 对象:{ key: 'value' }
  • ✅ 数组:[1, 2, 3]
  • ✅ React Element(作为 children

不可序列化的类型

  • ❌ 函数:() => {}
  • ❌ 类实例:new Date()new RegExp()
  • ❌ Symbol
  • ❌ Map、Set

常见陷阱

陷阱 1:忘记 "use client" 指令

问题:在 Client Component 中忘记添加 "use client"

// ❌ 错误:缺少 "use client"
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // 报错:useState 只能在 Client Component 中使用
  
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

// ✅ 正确:添加 "use client"
"use client";

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>;
}

陷阱 2:在 Client Component 中导入 Server Component

问题:Client Component 导入的组件也会变成 Client Component。

// app/PostContent.tsx(Server Component)
async function PostContent() {
  const post = await db.query('SELECT * FROM posts LIMIT 1');
  return <article>{post.content}</article>;
}
// ❌ 错误:在 Client Component 中导入
"use client";

import { PostContent } from './PostContent'; // ❌ PostContent 变成 Client Component

export function Modal() {
  return (
    <div className="modal">
      <PostContent /> {/* ❌ 会在客户端执行,无法访问数据库 */}
    </div>
  );
}
// ✅ 正确:通过 children 传递
"use client";

export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <div className="modal">
      {children} {/* ✅ PostContent 仍然是 Server Component */}
    </div>
  );
}
// app/page.tsx(Server Component)
import { Modal } from './Modal';
import { PostContent } from './PostContent';

export default function Page() {
  return (
    <Modal>
      <PostContent /> {/* ✅ 通过 children 传递 */}
    </Modal>
  );
}

陷阱 3:忘记 await 异步 Server Component

问题:Server Component 是 async 函数,但忘记 await

// ❌ 错误:忘记 await
async function PostList() {
  const posts = fetchPosts(); // ❌ 返回 Promise,不是数据
  
  return (
    <ul>
      {posts.map(post => ( // ❌ 报错:posts.map is not a function
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

// ✅ 正确:使用 await
async function PostList() {
  const posts = await fetchPosts(); // ✅ 等待 Promise 完成
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

陷阱 4:在 Server Component 中使用 use client

问题:在 Server Component 文件中添加 "use client",导致整个文件变成 Client Component。

// ❌ 错误:不需要交互的组件使用了 "use client"
"use client";

async function BlogPost({ postId }) {
  const post = await db.query('SELECT * FROM posts WHERE id = ?', [postId]);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// ✅ 正确:移除 "use client"
async function BlogPost({ postId }) {
  const post = await db.query('SELECT * FROM posts WHERE id = ?', [postId]);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

最佳实践

1. 默认使用 Server Component

原则:除非需要交互,否则使用 Server Component。

// ✅ 好的做法:展示型组件使用 Server Component
async function ProductList() {
  const products = await db.query('SELECT * FROM products');
  return <ul>{/* ... */}</ul>;
}

// ✅ 好的做法:交互型组件使用 Client Component
"use client";

function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(l => l + 1)}>Likes: {likes}</button>;
}

2. 将 Client Component 下沉到叶子节点

原则:尽量减少 Client Component 的范围。

// ❌ 不好的做法:整个页面都是 Client Component
"use client";

export default function Page() {
  return (
    <div>
      <Header />
      <ProductList />
      <LikeButton />
    </div>
  );
}

// ✅ 好的做法:只有 LikeButton 是 Client Component
export default function Page() {
  return (
    <div>
      <Header />
      <ProductList />
      <LikeButton /> {/* "use client" */}
    </div>
  );
}

3. 使用 Suspense 优化加载体验

原则:为慢速组件添加 Suspense。

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <Header />
      
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

4. 合理使用缓存

原则:根据数据更新频率选择缓存策略。

// 静态内容:永久缓存
fetch('https://api.example.com/about', {
  cache: 'force-cache',
});

// 半动态内容:定时缓存(ISR)
fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 },
});

// 动态内容:不缓存
fetch('https://api.example.com/user', {
  cache: 'no-store',
});

5. 使用 Tags 管理缓存

原则:为相关数据添加相同的 Tag,方便统一失效。

// 获取数据时添加 Tag
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['blog-posts'] },
  });
  return res.json();
}

// 更新时失效 Tag
async function createPost(formData: FormData) {
  "use server";
  
  await db.query('INSERT INTO posts ...');
  revalidateTag('blog-posts');
}

本章小结

Key Takeaways

  1. RSC 的设计动机:简化数据获取、消除 Waterfall、减少客户端 JS、直接访问后端资源
  2. RSC vs SSR:RSC 生成 RSC Payload(无需 Hydration),SSR 生成 HTML(需要 Hydration)
  3. Hybrid Rendering:混合使用 Server Components 和 Client Components,发挥各自优势
  4. 4 层缓存系统
    • Request Memoization(请求级去重)
    • Data Cache(跨请求持久化)
    • Full Route Cache(整页缓存)
    • Router Cache(客户端路由缓存)
  5. 流式渲染:使用 Suspense 分段发送 HTML,渐进式渲染
  6. RSC 的限制:不能使用 Hooks、事件监听器、浏览器 API,Props 必须可序列化
  7. 最佳实践:默认使用 Server Component,将 Client Component 下沉到叶子节点

下一步

在下一章(第 6 章),我们将深入 Client Components,掌握 "use client" 的使用规则、状态管理、与 Server Components 的协作模式。


参考资料

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页