React Server Components 原理
RSC 是 React 19 的核心特性,2025 面试必考
1. 什么是 Server Components?
1.1 核心概念
Server Components = 只在服务端运行的 React 组件,不发送任何 JS 到客户端。
┌─────────────────────────────────────────────────┐
│ 组件类型对比 │
├─────────────────────────────────────────────────┤
│ │
│ Server Component (默认) │
│ ├── 在服务端执行 │
│ ├── 可以直接访问数据库、文件系统 │
│ ├── 不发送 JS 到客户端 (零 Bundle) │
│ └── 不能使用 useState、useEffect、事件处理 │
│ │
│ Client Component ('use client') │
│ ├── 在客户端执行(也可 SSR) │
│ ├── 可以使用所有 Hooks │
│ ├── 可以添加交互(onClick 等) │
│ └── JS 发送到客户端 │
│ │
└─────────────────────────────────────────────────┘1.2 基本示例
jsx
// app/page.tsx - Server Component (默认)
async function BlogPage() {
// 直接访问数据库!
const posts = await db.query('SELECT * FROM posts');
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.summary}</p>
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
))}
</div>
);
}
// components/LikeButton.tsx - Client Component
'use client';
import { useState } from 'react';
export function LikeButton({ postId }) {
const [likes, setLikes] = useState(0);
return (
<button onClick={() => setLikes(l => l + 1)}>
❤️ {likes}
</button>
);
}2. 为什么需要 Server Components?
2.1 传统 CSR 的问题
┌─────────────────────────────────────────────────┐
│ 传统 CSR 问题 │
├─────────────────────────────────────────────────┤
│ │
│ Bundle Size 爆炸: │
│ ├── React + ReactDOM: ~40KB │
│ ├── 状态管理: ~10-30KB │
│ ├── UI 组件库: ~50-100KB │
│ ├── 工具库 (date-fns, lodash): ~20-50KB │
│ ├── 业务代码: ~100KB+ │
│ └── 总计: 300-500KB (gzip) │
│ │
│ 数据获取瀑布: │
│ Download HTML → Download JS → Execute JS │
│ → Fetch API → Render │
│ (串行,每步都要等) │
│ │
└─────────────────────────────────────────────────┘2.2 Server Components 解决方案
┌─────────────────────────────────────────────────┐
│ Server Components 优势 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 零 Bundle Size │
│ Server Component 不发送 JS │
│ 只发送渲染结果 (HTML + Flight 数据) │
│ │
│ 2. 直接数据访问 │
│ 无需 API 层,直接访问数据库 │
│ 减少网络往返 │
│ │
│ 3. 自动代码分割 │
│ Client Component 自动按需加载 │
│ │
│ 4. 安全性 │
│ 敏感逻辑在服务端,不暴露给客户端 │
│ │
└─────────────────────────────────────────────────┘3. 'use client' 和 'use server' 指令
3.1 'use client' - 客户端边界
jsx
// 文件顶部添加 'use client'
'use client';
// 这个文件及其导入的所有组件都是 Client Component
import { useState } from 'react';
import { Button } from './Button'; // 也变成 Client Component
export function Counter() {
const [count, setCount] = useState(0);
return <Button onClick={() => setCount(c => c + 1)}>{count}</Button>;
}3.2 'use server' - Server Actions
jsx
// actions.ts
'use server';
// 这个文件的所有导出函数都是 Server Actions
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({ title });
revalidatePath('/posts');
}
export async function deletePost(id: string) {
await db.posts.delete(id);
revalidatePath('/posts');
}3.3 边界规则
┌─────────────────────────────────────────────────┐
│ 组件边界规则 │
├─────────────────────────────────────────────────┤
│ │
│ Server Component 可以: │
│ ├── import Client Component ✅ │
│ ├── import Server Component ✅ │
│ └── 将 Server Component 作为 children 传递 ✅ │
│ │
│ Client Component 可以: │
│ ├── import Client Component ✅ │
│ └── 接收 Server Component 作为 children ✅ │
│ │
│ Client Component 不能: │
│ └── import Server Component ❌ │
│ │
└─────────────────────────────────────────────────┘jsx
// ✅ 正确:Server 导入 Client
// ServerComponent.tsx (Server)
import { ClientButton } from './ClientButton';
export function ServerComponent() {
return <ClientButton />;
}
// ✅ 正确:通过 children 传递
// ClientWrapper.tsx
'use client';
export function ClientWrapper({ children }) {
return <div onClick={...}>{children}</div>; // children 可以是 Server Component
}
// Page.tsx (Server)
import { ClientWrapper } from './ClientWrapper';
import { ServerContent } from './ServerContent';
export function Page() {
return (
<ClientWrapper>
<ServerContent /> {/* Server Component 通过 children 传递 */}
</ClientWrapper>
);
}4. 数据获取模式
4.1 Server Component 直接获取
jsx
// ✅ 推荐:数据获取在 Server Component
async function ProductPage({ id }) {
// 直接在组件中获取数据
const product = await db.products.findById(id);
const reviews = await db.reviews.findByProductId(id);
return (
<div>
<ProductInfo product={product} />
<ReviewList reviews={reviews} />
<AddReviewForm productId={id} /> {/* Client Component */}
</div>
);
}4.2 并行数据获取
jsx
async function Dashboard() {
// 并行获取,不阻塞
const [user, posts, notifications] = await Promise.all([
getUser(),
getPosts(),
getNotifications()
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<NotificationBell notifications={notifications} />
</div>
);
}4.3 结合 Suspense 流式渲染
jsx
import { Suspense } from 'react';
async function SlowComponent() {
const data = await fetchSlowData(); // 需要 3 秒
return <div>{data}</div>;
}
export function Page() {
return (
<div>
<h1>Dashboard</h1>
<FastComponent /> {/* 立即显示 */}
<Suspense fallback={<Skeleton />}>
<SlowComponent /> {/* 流式传输,3 秒后显示 */}
</Suspense>
</div>
);
}5. Flight 协议
5.1 什么是 Flight?
Flight 是 React 用于序列化 Server Component 渲染结果的协议。
Server Component 渲染
│
▼
Flight 格式 (类似 JSON,但支持更多类型)
│
▼
Stream 传输到客户端
│
▼
React 客户端解析并渲染5.2 Flight 数据示例
javascript
// Server Component 渲染结果的 Flight 格式
0:["$","div",null,{"children":[
["$","h1",null,{"children":"Hello"}],
["$","$L1",null,{}] // $L1 表示需要加载的 Client Component
]}]
1:I["./ClientComponent.js",["default"]] // Client Component 的加载指令5.3 Streaming 流式传输
┌─────────────────────────────────────────────────┐
│ Streaming SSR 流程 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 立即发送 Shell (HTML 框架) │
│ <html><body><div id="root"> │
│ │
│ 2. 流式发送内容 │
│ <script>$RC("B:1", "<div>Content</div>")</script>
│ │
│ 3. Suspense 边界完成后发送 │
│ <script>$RC("B:2", "<div>Slow</div>")</script>
│ │
│ 4. 发送 </body></html> 结束 │
│ │
└─────────────────────────────────────────────────┘6. 组件设计模式
6.1 最小化 Client Component
jsx
// ❌ 错误:整个组件都是 Client
'use client';
function ProductPage({ id }) {
const [quantity, setQuantity] = useState(1);
const product = useProduct(id); // 客户端获取
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<QuantitySelector value={quantity} onChange={setQuantity} />
</div>
);
}
// ✅ 正确:只有交互部分是 Client
// ProductPage.tsx (Server)
async function ProductPage({ id }) {
const product = await getProduct(id); // 服务端获取
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<QuantitySelector /> {/* 只有这个是 Client */}
</div>
);
}
// QuantitySelector.tsx
'use client';
function QuantitySelector() {
const [quantity, setQuantity] = useState(1);
return <input type="number" value={quantity} onChange={...} />;
}6.2 Props 序列化限制
jsx
// ❌ 不能传递函数给 Client Component
function ServerComponent() {
const handleClick = () => console.log('clicked');
return <ClientButton onClick={handleClick} />; // 错误!
}
// ✅ 使用 Server Actions
function ServerComponent() {
async function handleClick() {
'use server';
console.log('clicked on server');
}
return <ClientButton onClick={handleClick} />; // Server Action 可以
}
// 可序列化的 props:
// - 原始值: string, number, boolean, null, undefined
// - 数组和普通对象 (包含可序列化值)
// - Date, Map, Set (有限支持)
// - Server Actions (特殊处理)
// - React Elements (JSX)7. 缓存与重验证
7.1 数据缓存
jsx
// Next.js 中的缓存控制
async function getProduct(id) {
// 默认缓存
const res = await fetch(`/api/products/${id}`);
// 不缓存
const res = await fetch(`/api/products/${id}`, { cache: 'no-store' });
// 定时重验证
const res = await fetch(`/api/products/${id}`, {
next: { revalidate: 3600 } // 1小时
});
}7.2 重验证策略
jsx
import { revalidatePath, revalidateTag } from 'next/cache';
// Server Action 中重验证
async function createPost(formData) {
'use server';
await db.posts.create(...);
// 重验证特定路径
revalidatePath('/posts');
// 重验证特定标签
revalidateTag('posts');
}8. 面试高频问题
Q1: Server Component 和 SSR 的区别?
| 对比项 | SSR | Server Components |
|---|---|---|
| 运行时机 | 每次请求 | 每次请求或构建时 |
| 输出 | HTML 字符串 | React 树 (Flight) |
| Hydration | 全量 Hydration | Selective Hydration |
| 客户端 JS | 发送完整组件代码 | 只发送 Client Component |
| 状态 | Hydration 后有状态 | 永远无状态 |
Q2: 什么时候用 Server Component vs Client Component?
Server Component:
- 数据获取
- 访问后端资源(数据库、文件)
- 敏感信息(API keys)
- 大型依赖(markdown 解析)
Client Component:
- 交互(onClick, onChange)
- 状态(useState, useReducer)
- 生命周期(useEffect)
- 浏览器 API(localStorage, geolocation)
Q3: 为什么 Client Component 不能 import Server Component?
Server Component 只在服务端运行,客户端没有运行它的能力。如果 Client Component 直接 import,打包时会把 Server Component 代码包含进去,但客户端无法执行(可能包含数据库访问等)。
解决方案:通过 children 或 props 传递 Server Component。
Q4: Server Components 如何处理认证?
jsx
import { cookies } from 'next/headers';
async function AuthenticatedPage() {
const session = await getSession(cookies());
if (!session) {
redirect('/login');
}
const user = await getUser(session.userId);
return <Dashboard user={user} />;
}Q5: 如何调试 Server Components?
- 服务端日志:
console.log输出在服务端终端 - React DevTools:显示组件树,标记 Server/Client
- Network 面板:查看 Flight 数据流
- 错误边界:使用
error.tsx捕获错误