第 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>
  );
}

问题:

  • 需要手动管理 loading、error、data 状态
  • 需要处理竞态条件(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>
  );
}

优势:

  • ✅ 无需 useState、useEffect
  • ✅ 无需手动管理 loading、error 状态
  • ✅ 代码更简洁、更易读
  • ✅ 直接访问数据库,无需 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,包含 PostList 和 LikeButton 的占位符
  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 不能使用 useState、useEffect、useContext 等。

// ❌ 错误: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 不能使用 onClick、onChange 等。

// ❌ 错误: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 在服务端执行,无法访问 window、document、localStorage。

// ❌ 错误: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>

可序列化的类型:

  • ✅ 原始类型:string、number、boolean、null、undefined
  • ✅ 对象:{ 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 的协作模式。


参考资料

继续阅读

探索更多技术文章

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

全部文章 返回首页

「frontend」更多文章

  1. Vue 性能优化指南:虚拟列表、懒加载、渲染优化与 Core Web Vitals
  2. Nuxt.js 完全指南:Vue 全栈框架的 SSR、SSG、API 路由与部署实践
  3. Vue 测试深度指南:Vitest + Vue Test Utils + Playwright E2E 与 CI 集成