前言
React Server Components(简称 RSC)是 React 18 引入的核心特性,也是自 React Hooks 以来最重要的架构变革。它不仅仅是「服务端渲染」的新写法,而是一种全新的组件模型------让服务端能力成为 React 组件系统的原生特性,而非附加层。
本文将从原理、架构、实践三个维度,深度剖析 RSC 的设计思想与最佳实践。
一、为什么需要 Server Components?
1.1 传统 SSR 的局限性
在 RSC 出现之前,「服务端渲染」和「客户端渲染」是两个独立的概念:
css
传统 SSR(如 Next.js Pages Router、Nuxt 2):
┌─────────────────────────────────────────────────┐
│ │
│ 请求 ──→ 服务端渲染完整 HTML ──→ 发送 HTML │
│ │ │
│ ▼ │
│ 浏览器下载 HTML ──→ 显示首屏 │
│ │ │
│ ▼ │
│ 下载 JS Bundle ──→ 水合(Hydration) │
│ │ │
│ ▼ │
│ React 在客户端激活 ──→ 可交互 │
│ │
└─────────────────────────────────────────────────┘
问题:
✗ 服务端和客户端组件代码混合,难以区分
✗ 水合过程耗时,且无法跳过已渲染的静态内容
✗ 所有组件都打包进客户端 JS,即使是不需要交互的
✗ 数据获取逻辑分散,难以优化
1.2 RSC 的解决思路
RSC 的核心理念:组件应该在它「擅长」的地方运行。
arduino
RSC 架构:
┌──────────────────────────────────────────────────────────────┐
│ │
│ Server Component Client Component │
│ (服务端运行) (客户端运行) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ │ │ │ │
│ │ • 数据库访问 │ │ • useState │ │
│ │ • 文件系统 │ │ • useEffect │ │
│ │ • 敏感 API │ │ • 浏览器 API │ │
│ │ • 大型依赖 │ │ • 事件监听 │ │
│ │ • 零 JS 输出 │ │ • 交互逻辑 │ │
│ │ │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ 直接访问数据源 等待交互 │
│ 生成 HTML 片段 渲染 UI │
│ │
└──────────────────────────────────────────────────────────────┘
核心优势:
| 优势 | 说明 |
|---|---|
| 零客户端 JS | Server Components 不打包到客户端,显著减少 bundle 体积 |
| 直接访问后端 | 可直接查询数据库、读取文件系统,无需 API 层 |
| 自动代码分割 | 每个组件独立序列化,按需加载 |
| 流式渲染 | 边生成边返回,用户更快看到内容 |
二、核心原理深度剖析
2.1 组件分类体系
RSC 引入了清晰的组件分类:
arduino
React 组件类型
│
├── Server Components(默认)
│ ├── 特性:异步、服务端执行、可访问后端资源
│ ├── 限制:不能使用 hooks、事件处理、浏览器 API
│ └── 产物:序列化为 RSC Payload,不产生 JS
│
├── Client Components('use client' 标记)
│ ├── 特性:可使用 hooks、交互、浏览器 API
│ ├── 限制:不能直接访问服务端资源
│ └── 产物:打包进 JS Bundle,需要水合
│
└── Server Actions('use server' 标记)
├── 特性:在服务端执行的异步函数
├── 用途:表单处理、数据 mutations
└── 产物:网络请求,自动处理序列化
2.2 RSC Payload 机制
RSC 的数据传输格式是理解其原理的关键:
json
┌─────────────────────────────────────────────────────────────┐
│ RSC Payload 结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1:["$","div",null,{"children":[ │
│ 2:["$","ul",null,{"children":[ │
│ 3:["$","li",null,{"children":"产品A"}], │
│ 4:["$","li",null,{"children":"产品B"}], │
│ 5:["$","li",null,{"children":"产品C"}] │
│ ]}] │
│ ]} │
│ │
│ 解读: │
│ - $ 是 RSC 模块标识符 │
│ - 数字是模块引用 ID │
│ - 对象是组件 props │
│ - 字符串是文本节点 │
│ │
└─────────────────────────────────────────────────────────────┘
这个格式实现了:
- 紧凑传输:比 JSON 更小
- 懒加载引用:只有被 Client Component 引用的才需要 JS
- 增量渲染:可以流式发送
2.3 服务端与客户端的边界
RSC 的边界规则非常精确:
markdown
✅ Server Component 中可以:
- 异步 await fetch()
- 直接访问数据库
- 读取 .env 文件
- 使用任何 Node.js API
❌ Server Component 中不能:
- useState / useEffect
- 事件处理 onClick
- window / document
- localStorage
✅ Client Component 中可以:
- useState / useEffect
- 事件处理
- 浏览器 API
❌ Client Component 中不能:
- 直接访问数据库
- 直接调用服务端 API(必须通过 fetch)
2.4 组件树中的嵌套规则
Server 嵌套 Client
tsx
// ServerComponent.tsx(服务端)
import { ClientButton } from './ClientButton';
export default async function ServerComponent() {
// 服务端直接获取数据
const data = await db.query('SELECT * FROM products');
return (
<div>
<h1>产品列表</h1>
{/* ✅ 允许:Server 包裹 Client */}
<ClientButton onClick={() => console.log('clicked')} />
</div>
);
}
Client 中的 Server 组件
tsx
// ClientWrapper.tsx(客户端)
'use client';
import ServerComponent from './ServerComponent'; // ❌ 不允许!
export default function ClientWrapper() {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>切换</button>
{show && <ServerComponent />} {/* ❌ 错误:Client 不能导入 Server */}
</div>
);
}
正确做法:使用 children prop
tsx
// ServerParent.tsx
import { ClientChild } from './ClientChild';
export default async function ServerParent() {
const data = await fetchData();
return (
<ClientChild>
{/* ✅ 正确:children 会在 Client 外部渲染 */}
<ServerContent data={data} />
</ClientChild>
);
}
// ClientChild.tsx
'use client';
export function ClientChild({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
点击 {count} 次
</button>
{/* children 已经是渲染好的内容 */}
{children}
</div>
);
}
三、Next.js App Router 实战
3.1 目录结构
python
app/
├── layout.tsx # 根布局(Server Component)
├── page.tsx # 首页(Server Component)
├── globals.css
│
├── products/
│ ├── page.tsx # 产品列表(Server Component)
│ ├── loading.tsx # 加载状态
│ ├── error.tsx # 错误边界
│ └── [id]/
│ ├── page.tsx # 产品详情(Server Component)
│ └── actions.ts # Server Actions
│
└── dashboard/
├── layout.tsx # Dashboard 布局
├── page.tsx
└── analytics/
└── page.tsx # Analytics(可能需要 Client Component)
3.2 数据获取模式
直接在 Server Component 中获取数据
tsx
// app/products/page.tsx
export default async function ProductsPage() {
// 方式一:直接查询数据库
const products = await db.query('SELECT * FROM products');
// 方式二:使用 Prisma
const products = await prisma.product.findMany();
// 方式三:fetch(自动 dedupe)
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // 1小时缓存
});
const products = await res.json();
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
并行数据获取
tsx
export default async function DashboardPage() {
// ❌ 串行:慢
// const user = await getUser();
// const stats = await getStats();
// ✅ 并行:快
const [user, stats] = await Promise.all([
getUser(),
getStats()
]);
// ✅ 更优:使用 Promise.allSettled 避免一个失败导致全部失败
const [userResult, statsResult] = await Promise.allSettled([
getUser(),
getStats()
]);
return (
<div>
<h1>Welcome, {userResult.value?.name}</h1>
<Stats data={statsResult.value} />
</div>
);
}
3.3 流式渲染
Suspense + 异步组件
tsx
// app/page.tsx
import { Suspense } from 'react';
import { ProductList, ProductRecommendations, ReviewSummary } from './components';
// 快速内容立即渲染
export default function ProductPage() {
return (
<div>
<h1>产品详情</h1>
{/* 慢的内容使用 Suspense */}
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<ProductRecommendations />
</Suspense>
<Suspense fallback={<ReviewSkeleton />}>
<ReviewSummary />
</Suspense>
</div>
);
}
// components/ProductList.tsx
async function ProductList() {
// 模拟慢查询
const products = await fetchProductsSlow();
return <div>{/* ... */}</div>;
}
Streaming HTML 效果
html
<!-- 立即返回 -->
<div><h1>产品详情</h1></div>
<!-- 稍后流式插入 -->
<div><div class="skeleton">加载中...</div></div>
<!-- ↑ 被替换为实际内容 -->
<div><div class="skeleton">加载中...</div></div>
<!-- ↑ 被替换为实际内容 -->
四、Server Actions 深度应用
4.1 基础用法
tsx
// app/actions.ts
'use server';
// 普通的异步函数,自动成为 Server Action
export async function createProduct(formData: FormData) {
const name = formData.get('name');
const price = Number(formData.get('price'));
// 直接访问数据库
const product = await db.product.create({
data: { name, price }
});
// 重定向或返回
revalidatePath('/products');
return { success: true, product };
}
// app/products/new/page.tsx
import { createProduct } from '../actions';
export default function NewProductPage() {
async function handleSubmit(formData: FormData) {
'use server'; // 内联 Server Action
// ...
}
return (
<form action={createProduct}>
<input name="name" type="text" required />
<input name="price" type="number" step="0.01" required />
<button type="submit">创建产品</button>
</form>
);
}
4.2 带验证的 Server Action
tsx
// lib/actions.ts
'use server';
import { z } from 'zod';
const ProductSchema = z.object({
name: z.string().min(2, '名称至少2个字符'),
price: z.number().positive('价格必须为正数'),
category: z.enum(['electronics', 'clothing', 'books']),
});
export async function createProduct(prevState: any, formData: FormData) {
// 1. 验证数据
const result = ProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
category: formData.get('category'),
});
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
message: '表单验证失败',
};
}
// 2. 执行业务逻辑
try {
const product = await db.product.create({ data: result.data });
revalidatePath('/products');
return { message: '创建成功', product };
} catch (error) {
return { message: '创建失败,请重试' };
}
}
4.3 useActionState(formerly useFormState)
tsx
'use client';
import { useActionState } from 'react';
import { createProduct } from './actions';
import { initialState } from './store';
export function ProductForm() {
const [state, formAction, isPending] = useActionState(
createProduct,
initialState
);
return (
<form action={formAction}>
<input name="name" />
{state.errors?.name && (
<span className="error">{state.errors.name}</span>
)}
<input name="price" type="number" />
{state.errors?.price && (
<span className="error">{state.errors.price}</span>
)}
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '创建'}
</button>
{state.message && (
<p className={state.errors ? 'error' : 'success'}>
{state.message}
</p>
)}
</form>
);
}
五、性能优化实战
5.1 Bundle 体积优化
Before RSC
lua
用户的 JS Bundle:
├── react + react-dom 50kb
├── 你的应用代码 200kb
│ ├── ProductList 30kb (只是展示数据)
│ ├── ProductCard 20kb (只是展示数据)
│ ├── CartButton 10kb (需要交互)
│ └── CheckoutForm 40kb (需要交互)
└── 第三方库 150kb
├── date-fns 80kb (只用了一个函数)
└── numeral 20kb (只用了一个函数)
总计:450kb(用户必须下载)
After RSC
markdown
用户的 JS Bundle:
├── react + react-dom 50kb
├── 你的应用代码 60kb(只有交互组件)
│ ├── CartButton 10kb
│ └── CheckoutForm 40kb
└── 第三方库 30kb(只有客户端需要的)
总计:140kb(减少 70%!)
Server Component 产物:
- ProductList → 直接渲染为 HTML,零 JS
- date-fns → 只在服务端运行,不需要发送给客户端
5.2 大型依赖处理
常见场景:marked(Markdown 解析)
tsx
// ❌ 错误:marked 被打包进客户端
import { marked } from 'marked';
function BlogPost({ content }: { content: string }) {
const html = marked(content); // 这会在客户端执行!
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
tsx
// ✅ 正确:在 Server Component 中处理
import { marked } from 'marked';
async function BlogPost({ content }: { content: string }) {
// 在服务端执行,marked 不进入客户端 bundle
const html = await marked(content);
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
5.3 缓存策略
tsx
// app/products/page.tsx
// 方式一:按时间重新验证
export default async function ProductsPage() {
const products = await fetch('...', {
next: { revalidate: 3600 } // 每小时重新生成
});
}
// 方式二:按路径重新验证
import { revalidatePath } from 'next/cache';
export async function createProduct(data: FormData) {
await db.product.create({ data });
revalidatePath('/products'); // 清除 /products 的缓存
revalidatePath('/products/[id]', 'page'); // 精确清除
}
// 方式三:按标签重新验证(需要 Data Cache)
export default async function ProductsPage() {
const products = await fetch('https://...', {
next: { tags: ['products'] } // 打上标签
});
}
export async function updateProduct(id: string, data: any) {
await db.product.update({ where: { id }, data });
revalidateTag('products'); // 清除 tagged 缓存
}
六、常见问题与解决方案
Q1: 如何调试 Server Component?
tsx
// Server Component 中可以使用 console.log
async function ServerComponent() {
const data = await fetchData();
console.log('服务端数据:', data); // 输出在服务器终端
return <div>{data.name}</div>;
}
Q2: 如何处理用户认证?
tsx
// middleware.ts(Edge Runtime)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};
tsx
// Server Component 中获取用户
import { getServerUser } from '@/lib/auth';
export default async function DashboardPage() {
const user = await getServerUser(); // 服务端直接获取
if (!user) redirect('/login');
return <div>Welcome, {user.name}</div>;
}
Q3: 何时使用 Client Component?
tsx
// 需要客户端交互 → 'use client'
'use client';
export function LikeButton({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount(c => c + 1)}>
👍 {count}
</button>
);
}
// 需要浏览器 API → 'use client'
'use client';
export function ScrollIndicator() {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 100);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return scrolled ? <TopBar /> : null;
}
Q4: 第三方组件库兼容性问题?
tsx
// 问题:很多 UI 库使用 useState/useEffect
// 但它们本身不需要交互,只是展示
// 解决方案一:创建 Client Wrapper
// components/MUIPaper.tsx
'use client';
import { Paper } from '@mui/material'; // 客户端版本
export function PaperWrapper({ children, ...props }) {
return <Paper {...props}>{children}</Paper>;
}
// app/page.tsx
import { PaperWrapper } from '@/components/MUIPaper';
export default function Page() {
return (
<PaperWrapper elevation={2}>
{/* 任何内容,包括 Server Components */}
<ServerContent />
</PaperWrapper>
);
}
// 解决方案二:使用 RSC 友好的库
// 如:shadcn/ui(本身都是 Server Component 友好设计)
七、架构决策指南
何时选择 RSC?
✅ 强烈推荐 RSC 的场景:
├─ 数据密集型页面(Dashboard、分析页)
├─ 内容为主的应用(博客、文档、电商)
├─ SEO 敏感页面(产品页、文章页)
├─ 需要访问数据库或文件系统
└─ 希望最小化客户端 JS
⚠️ 需要谨慎评估的场景:
├─ 强交互应用(在线文档、设计工具)
├─ 实时性要求高(聊天、游戏)
├─ 复杂状态管理(复杂表单、多人协作)
└─ 团队对新技术不熟悉
架构决策树
arduino
新的 React 页面/组件:
│
├─ 需要用户交互(onClick、useState)?
│ ├─ 是 → Client Component
│ └─ 否 → 继续判断
│
├─ 需要浏览器 API(window、localStorage)?
│ ├─ 是 → Client Component
│ └─ 否 → Server Component
│
└─ 不确定?
→ 默认 Server Component,按需升级为 Client
结语
React Server Components 代表了 React 团队对「组件」概念的重新思考:让组件在它最适合的环境中运行。
arduino
RSC 的核心价值
│
├─ 性能 ── 减少客户端 JS,提升首屏速度
├─ 体验 ── Streaming SSR,更快的 TTI
├─ 架构 ── 清晰的 Server/Client 边界
├─ 简化 ── 直接访问数据源,无需 API 层
└─ 扩展 ── 服务端能力赋能前端开发
掌握 RSC,不仅是学习一个新特性,更是理解现代前端架构演进方向的关键。
参考资料:React Server Components 官方文档、Next.js App Router 文档、RSC 规范