本章是 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 的设计目标:
- 简化数据获取:直接在组件中使用
async/await - 消除 Waterfall:服务端并行获取数据
- 减少客户端 JS:服务端组件代码不发送到客户端
- 直接访问后端资源:数据库、文件系统、内部 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 的作用:
- React 接管服务端生成的 HTML
- 重新执行所有组件代码
- 添加事件监听器
- 建立状态管理
问题:
- 需要下载完整的 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>
);
}
服务端执行:
- 执行组件函数
- 生成 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:
- 解析 RSC Payload
- 直接渲染为 DOM
- 无需 Hydration
核心差异对比
| 维度 | 传统 SSR | RSC |
|---|---|---|
| 服务端输出 | 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>
);
}
渲染流程:
- 服务端执行
HomePage,获取posts - 生成 RSC Payload,包含
PostList和LikeButton的占位符 - 客户端渲染 DOM
- 下载
LikeButton的 JS - 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>
);
}
服务端执行:
- 渲染
<Header />(立即完成) - 渲染
<Suspense>(显示 fallback) - 渲染
<MainContent />(异步,等待数据) - 渲染
<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():读取 PromiseuseId():生成唯一 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
- RSC 的设计动机:简化数据获取、消除 Waterfall、减少客户端 JS、直接访问后端资源
- RSC vs SSR:RSC 生成 RSC Payload(无需 Hydration),SSR 生成 HTML(需要 Hydration)
- Hybrid Rendering:混合使用 Server Components 和 Client Components,发挥各自优势
- 4 层缓存系统:
- Request Memoization(请求级去重)
- Data Cache(跨请求持久化)
- Full Route Cache(整页缓存)
- Router Cache(客户端路由缓存)
- 流式渲染:使用 Suspense 分段发送 HTML,渐进式渲染
- RSC 的限制:不能使用 Hooks、事件监听器、浏览器 API,Props 必须可序列化
- 最佳实践:默认使用 Server Component,将 Client Component 下沉到叶子节点
下一步
在下一章(第 6 章),我们将深入 Client Components,掌握 "use client" 的使用规则、状态管理、与 Server Components 的协作模式。
参考资料
- React 官方文档:Server Components
- Next.js 官方文档:Caching
- Next.js 官方文档:Streaming and Suspense
- Vercel Blog: React Server Components
- GitHub: React Server Components RFC
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。