React Server Components 原理精讲
引言:RSC 是什么,不是什么
React Server Components(RSC)可能是 React 近年来最重要的架构演进。但围绕它的误解也最多:有人以为它就是 SSR 的新名字,有人以为它会取代客户端 React,还有人以为它只是 Next.js 的专属功能。
这篇文章的目标是帮你建立对 RSC 的准确理解——从设计动机到工作原理,从使用方式到最佳实践。
第一部分:为什么需要 RSC
1.1 传统 React 应用的痛点
让我们从一个典型的 React 应用说起:
// 传统客户端 React 应用
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
fetch(`/api/products/${productId}`),
fetch(`/api/products/${productId}/reviews`)
])
.then(([p, r]) => Promise.all([p.json(), r.json()]))
.then(([product, reviews]) => {
setProduct(product);
setReviews(reviews);
setLoading(false);
});
}, [productId]);
if (loading) return <Skeleton />;
return (
<div>
<ProductDetail product={product} />
<ReviewList reviews={reviews} />
</div>
);
}
这个模式存在几个根本性问题:
┌──────────────────────────────────────────────────────────────┐
│ 传统 SPA 的问题 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 客户端数据瀑布流 │
│ 浏览器 → 下载 JS → 执行 → 发起请求 → 等待响应 → 渲染 │
│ 用户看到内容的时间 = 所有步骤的总和 │
│ │
│ 2. Bundle 膨胀 │
│ - React + ReactDOM: ~40KB │
│ - 组件代码: 可能几百 KB │
│ - 依赖库: 可能几 MB │
│ 所有这些都要下载到客户端 │
│ │
│ 3. 数据获取与组件分离 │
│ - 数据获取在 useEffect 中 │
│ - 容易产生 N+1 问题 │
│ - 难以优化和预取 │
│ │
└──────────────────────────────────────────────────────────────┘
1.2 SSR 解决了什么,没解决什么
传统 SSR 解决了首屏渲染问题,但引入了新问题:
// 传统 SSR (如 Next.js Pages Router)
export async function getServerSideProps() {
const product = await fetchProduct(id);
const reviews = await fetchReviews(id);
return { props: { product, reviews } };
}
function ProductPage({ product, reviews }) {
return (
<div>
<ProductDetail product={product} />
<ReviewList reviews={reviews} />
</div>
);
}
SSR 的问题:
┌──────────────────────────────────────────────────────────────┐
│ 传统 SSR 的问题 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 双重渲染 │
│ - 服务端渲染一次生成 HTML │
│ - 客户端 hydration 又渲染一次 │
│ - 同样的组件代码执行两遍 │
│ │
│ 2. 全量 Hydration │
│ - 即使是静态内容也需要 hydrate │
│ - 客户端 JS 依然很大 │
│ - TTI (可交互时间) 受影响 │
│ │
│ 3. 数据获取仍集中在页面级 │
│ - getServerSideProps 只在页面组件中 │
│ - 子组件无法独立获取数据 │
│ - Props drilling 问题依然存在 │
│ │
└──────────────────────────────────────────────────────────────┘
1.3 RSC 的设计目标
RSC 试图从根本上解决这些问题:
┌──────────────────────────────────────────────────────────────┐
│ RSC 的核心理念 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 组件级别的渲染位置选择 │
│ - 每个组件可以独立决定在服务端还是客户端渲染 │
│ - 而不是整个页面统一决定 │
│ │
│ 2. 零客户端 JS 的服务端组件 │
│ - Server Components 的代码不会发送到客户端 │
│ - 只发送渲染结果 │
│ │
│ 3. 数据获取与组件共置 │
│ - 组件可以直接 await 数据 │
│ - 无需 useEffect 或状态管理 │
│ - 数据获取自动并行 │
│ │
└──────────────────────────────────────────────────────────────┘
第二部分:RSC 工作原理
2.1 两种组件类型
RSC 引入了两种组件类型:
// Server Component (默认)
// - 只在服务端执行
// - 可以 async/await
// - 可以直接访问数据库、文件系统等
// - 不能使用 hooks (useState, useEffect 等)
// - 不能使用浏览器 API
async function ProductDetail({ id }) {
// 直接在组件中获取数据
const product = await db.products.findUnique({ where: { id } });
// 可以使用服务端资源
const description = await fs.readFile(`./data/${id}.md`);
return (
<div>
<h1>{product.name}</h1>
<Markdown content={description} />
</div>
);
}
// Client Component
// - 使用 'use client' 指令标记
// - 在服务端和客户端都执行
// - 可以使用所有 React 特性
// - 可以使用浏览器 API
'use client'
function AddToCartButton({ productId }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
await addToCart(productId);
setPending(false);
}
return (
<button onClick={handleClick} disabled={pending}>
{pending ? '添加中...' : '加入购物车'}
</button>
);
}
2.2 组件树与边界
// 组件树中的 Server/Client 边界
// ✅ 有效的组件树
<ServerComponent> // Server
<AnotherServer> // Server
<ClientComponent> // Client - 这是边界
<ChildComponent /> // 自动成为 Client
</ClientComponent>
</AnotherServer>
</ServerComponent>
// Server Component 可以导入 Client Component
import { ClientButton } from './ClientButton'
async function ServerPage() {
const data = await fetchData();
return (
<div>
<h1>{data.title}</h1>
<ClientButton /> {/* Client 组件作为子组件 */}
</div>
);
}
// ❌ 错误:Client Component 不能导入 Server Component
'use client'
import { ServerWidget } from './ServerWidget' // 错误!
function ClientPage() {
return <ServerWidget />; // 不允许
}
// ✅ 正确:通过 children 传递
'use client'
function ClientWrapper({ children }) {
const [show, setShow] = useState(true);
return show ? children : null; // children 可以是 Server Component
}
// 使用
<ClientWrapper>
<ServerWidget /> {/* 通过 props 传入,可以工作 */}
</ClientWrapper>
2.3 RSC Payload:渲染结果的传输格式
RSC 的核心创新之一是引入了新的传输格式:
// RSC Payload 示例
// 这不是 HTML,而是一种特殊的序列化格式
/*
0:["$","div",null,{"children":[
["$","h1",null,{"children":"商品详情"}],
["$","$L1",null,{"productId":"123"}],
["$","p",null,{"children":"这是一段描述..."}]
]}]
1:I["./ClientButton.js",["ClientButton"]]
*/
// 解读:
// - 0: 是 Server Component 的渲染结果
// - $L1: 表示一个 Client Component 的占位符
// - 1:I: 是 Client Component 的模块引用
// 这种格式的优势:
// 1. 可流式传输(不需要等待全部完成)
// 2. 可增量更新(只更新变化的部分)
// 3. 保持组件树结构(便于 reconciliation)
2.4 渲染流程详解
┌──────────────────────────────────────────────────────────────┐
│ RSC 渲染流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 服务端渲染阶段 │
│ ┌─────────────┐ │
│ │ 请求到达 │ │
│ └──────┬──────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 渲染 Server 组件树 │ │
│ │ (执行 async 函数) │ │
│ └──────┬──────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 遇到 Client 组件 │ │
│ │ 生成占位符和引用 │ │
│ └──────┬──────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 生成 RSC Payload │ │
│ │ (可流式传输) │ │
│ └──────┬──────────────┘ │
│ │
│ 2. 客户端处理阶段 │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 接收 RSC Payload │ │
│ └──────┬──────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ 重建虚拟 DOM 树 │ │
│ │ 加载 Client 组件 │ │
│ └──────┬──────────────┘ │
│ ↓ │
│ ┌─────────────────────┐ │
│ │ Hydrate Client 组件 │ │
│ │ (选择性 Hydration) │ │
│ └─────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
第三部分:实战中的边界划分
3.1 决定组件类型的原则
// 问:这个组件应该是 Server 还是 Client?
// 判断标准 1:是否需要浏览器 API?
function ComponentA() {
// 使用 localStorage - 需要 'use client'
const theme = localStorage.getItem('theme');
return <div className={theme}>...</div>;
}
// 判断标准 2:是否需要事件处理?
function ComponentB() {
// onClick 需要 'use client'
return <button onClick={() => alert('clicked')}>Click</button>;
}
// 判断标准 3:是否需要 React Hooks?
function ComponentC() {
// useState 需要 'use client'
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
// 判断标准 4:是否只是展示数据?
async function ComponentD({ id }) {
// 纯展示 - 保持 Server Component
const data = await fetchData(id);
return <div>{data.title}</div>;
}
3.2 常见模式
模式 1:提升 Client 边界
// ❌ 整个组件变成 Client
'use client'
function ProductCard({ product }) {
return (
<div>
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.description}</p> {/* 大段静态内容 */}
<button onClick={() => addToCart(product.id)}>
加入购物车
</button>
</div>
);
}
// ✅ 只将交互部分变成 Client
function ProductCard({ product }) {
return (
<div>
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>{product.description}</p> {/* 保持在 Server */}
<AddToCartButton productId={product.id} /> {/* 只有按钮是 Client */}
</div>
);
}
'use client'
function AddToCartButton({ productId }) {
return (
<button onClick={() => addToCart(productId)}>
加入购物车
</button>
);
}
模式 2:组合而非继承
// ✅ 使用 children 传递 Server 组件
'use client'
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal">
<button onClick={onClose}>关闭</button>
{children} {/* children 可以是 Server Component */}
</div>
);
}
// 使用
async function ProductPage() {
const product = await fetchProduct(id);
return (
<Modal isOpen={true} onClose={handleClose}>
{/* 这个组件在服务端渲染 */}
<ProductDetail product={product} />
</Modal>
);
}
模式 3:Context Provider 模式
// Context Provider 必须是 Client Component
// 但这不意味着所有子组件都必须是 Client
// providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }) {
return (
<ThemeProvider>
{children} {/* children 可以是 Server Components */}
</ThemeProvider>
);
}
// layout.tsx (Server Component)
import { Providers } from './providers'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>
{children} {/* 页面仍然可以是 Server Component */}
</Providers>
</body>
</html>
);
}
3.3 性能优化策略
// 策略 1:延迟加载 Client 组件
import dynamic from 'next/dynamic'
// 重型编辑器只在需要时加载
const HeavyEditor = dynamic(() => import('./HeavyEditor'), {
loading: () => <p>加载编辑器...</p>,
ssr: false // 禁用 SSR,减少服务端负担
});
// 策略 2:使用 Suspense 流式加载
async function Dashboard() {
return (
<div>
<Suspense fallback={<StatsSkeleton />}>
<Stats /> {/* 异步加载 */}
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<Chart /> {/* 独立的流 */}
</Suspense>
</div>
);
}
// 策略 3:预加载数据
import { preload } from 'react-dom'
async function ProductPage({ id }) {
// 预加载图片
preload(`/images/products/${id}.jpg`, { as: 'image' });
const product = await fetchProduct(id);
return <ProductDetail product={product} />;
}
第四部分:数据流与状态管理
4.1 Server Components 中的数据获取
// 直接 async/await
async function UserProfile({ userId }) {
const user = await db.users.findUnique({ where: { id: userId } });
const posts = await db.posts.findMany({ where: { authorId: userId } });
return (
<div>
<h1>{user.name}</h1>
<PostList posts={posts} />
</div>
);
}
// 并行数据获取
async function Dashboard() {
// 这些请求会并行执行
const [stats, users, orders] = await Promise.all([
fetchStats(),
fetchUsers(),
fetchOrders()
]);
return (
<div>
<StatsCard stats={stats} />
<UserTable users={users} />
<OrderTable orders={orders} />
</div>
);
}
// 使用 fetch 缓存
async function ProductList() {
// 相同的请求会自动去重
const products = await fetch('/api/products', {
next: { revalidate: 60 } // 缓存 60 秒
}).then(r => r.json());
return products.map(p => <ProductCard key={p.id} product={p} />);
}
4.2 Server Actions:从客户端修改服务端数据
// actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function addToCart(productId: string) {
// 这个函数在服务端执行
await db.cart.create({
data: { productId, userId: getCurrentUser().id }
});
// 重新验证购物车页面的缓存
revalidatePath('/cart');
}
export async function updateProfile(formData: FormData) {
const name = formData.get('name');
const email = formData.get('email');
await db.users.update({
where: { id: getCurrentUser().id },
data: { name, email }
});
revalidatePath('/profile');
}
// 在 Client Component 中使用
'use client'
function AddToCartButton({ productId }) {
async function handleClick() {
await addToCart(productId);
// 页面会自动更新
}
return <button onClick={handleClick}>加入购物车</button>;
}
// 在表单中使用
function ProfileForm({ user }) {
return (
<form action={updateProfile}>
<input name="name" defaultValue={user.name} />
<input name="email" defaultValue={user.email} />
<button type="submit">保存</button>
</form>
);
}
4.3 Client State vs Server State
// Client State:UI 状态、用户输入
'use client'
function SearchBox() {
// 这是客户端状态 - 即时响应用户输入
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
if (query.length > 2) {
fetchSuggestions(query).then(setSuggestions);
}
}, [query]);
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SuggestionList items={suggestions} />
</div>
);
}
// Server State:业务数据
async function ProductPage({ id }) {
// 这是服务端状态 - 来自数据库
const product = await fetchProduct(id);
return (
<div>
<h1>{product.name}</h1>
<SearchBox /> {/* 客户端状态组件 */}
</div>
);
}
// 混合场景:乐观更新
'use client'
function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [pending, setPending] = useState(false);
async function handleLike() {
// 乐观更新
setLikes(prev => prev + 1);
setPending(true);
try {
await likePost(postId); // Server Action
} catch {
setLikes(prev => prev - 1); // 回滚
} finally {
setPending(false);
}
}
return (
<button onClick={handleLike} disabled={pending}>
❤️ {likes}
</button>
);
}
第五部分:常见误区与最佳实践
5.1 误区澄清
// 误区 1:RSC 就是 SSR
// ❌ 错误理解
// RSC 和 SSR 是不同概念
// SSR:组件在服务端渲染成 HTML,然后客户端 hydrate
// RSC:Server Components 只在服务端运行,Client Components 正常 hydrate
// 误区 2:所有组件都应该是 Server Component
// ❌ 不现实
// 需要交互的组件必须是 Client Component
// 误区 3:'use client' 意味着只在客户端运行
// ❌ 部分正确
// Client Components 首次渲染时也在服务端执行(用于生成 HTML)
// 误区 4:Server Component 不能有子 Client Component
// ❌ 错误
// Server Component 可以导入和渲染 Client Component
// 误区 5:添加 'use client' 后整个文件都是客户端代码
// ✅ 正确
// 'use client' 是文件级别的声明
5.2 最佳实践总结
// ✅ 实践 1:默认使用 Server Components
// 只有需要时才标记 'use client'
// ✅ 实践 2:将 Client 边界推到叶子节点
// 最小化 Client Components 的范围
// ✅ 实践 3:使用 composition 模式
// 通过 children 传递 Server Components
// ✅ 实践 4:分离数据和交互
async function ProductCard({ id }) {
const product = await fetchProduct(id); // 数据获取在 Server
return (
<Card>
<CardContent product={product} /> {/* 静态内容在 Server */}
<CardActions productId={id} /> {/* 交互在 Client */}
</Card>
);
}
// ✅ 实践 5:使用 Suspense 处理异步
async function Page() {
return (
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
);
}
// ✅ 实践 6:避免在渲染时传递函数给 Client Components
// ❌
<ClientComponent onAction={() => serverAction()} />
// ✅
<ClientComponent actionId="specific-action" />
// 然后在 ClientComponent 中导入并调用 serverAction
结语:范式转变的意义
RSC 不只是一个新功能,它代表了 React 架构的范式转变:从"所有代码都在客户端运行"到"代码在最适合的地方运行"。
这个转变的意义在于:
- 更好的性能:减少客户端 JS,加快首屏
- 更好的开发体验:数据获取变得简单直观
- 更好的用户体验:流式渲染,更快的交互
学习 RSC 需要时间和实践,但理解其原理会让你在日常开发中做出更好的决策。


