React Server Components 原理精讲
React Server Components (RSC) 是 React 19 + Next.js 16 的核心创新——组件在服务端渲染,零 JS 发送到浏览器。但它不是 SSR 的改名,而是全新的架构范式。本章从 RSC 的设计动机讲起,彻底讲清 Server/Client 边界、序列化规则、RSC Payload 格式、以及常见的组合模式。
1. RSC 的设计动机
1.1 传统 React 的问题
在 RSC 之前,所有 React 组件都运行在浏览器端:
传统 React 应用的请求流程:
1. 浏览器请求页面 → 服务端返回空 HTML + JS Bundle
2. 浏览器下载 JS Bundle(可能 500KB+)
3. JS 执行 → React 渲染 → 用户看到页面
4. 组件内发起 API 请求获取数据
5. 数据返回 → 重新渲染 → 最终内容展示
问题:
├── JS Bundle 太大(所有组件的代码都发送到浏览器)
├── 数据瀑布(先渲染组件,再请求数据,再渲染)
├── 客户端-服务器往返太多
└── 敏感逻辑(数据库查询、API Key)暴露在客户端
1.2 RSC 的解决方案
RSC 的请求流程:
1. 浏览器请求页面 → 服务端执行 Server Components
2. Server Components 直接访问数据库/API(零延迟)
3. 渲染结果序列化为 RSC Payload → 流式发送到浏览器
4. 浏览器只接收:HTML + Client Components 的 JS
结果:
├── JS Bundle 减少 30-70%(Server Components 的 JS 不发送)
├── 数据获取零延迟(Server 直连数据库,无网络往返)
├── 流式渲染(先到的部分先显示)
└── 敏感逻辑永远在服务端
1.3 RSC ≠ SSR
这是最常见的误解:
| 维度 | SSR | RSC |
|---|---|---|
| 运行位置 | 服务端渲染 → 发送 HTML + 全部 JS | 服务端渲染 → 只发送 HTML + Client JS |
| JS Bundle | 全部组件的 JS 都发送到客户端 | 只发送 Client Components 的 JS |
| Hydration | 全页面 Hydration(所有组件) | 只 Hydrate Client Components |
| 交互性 | Hydration 后所有组件可交互 | Server Components 永远不可交互 |
| 数据获取 | getServerSideProps(页面级) | 组件级 async/await |
| 组件模型 | 所有组件本质上是客户端组件 | 区分 Server 和 Client 组件 |
2. Server Components vs Client Components
2.1 默认规则
在 App Router 中,所有组件默认是 Server Components:
// 默认就是 Server Component — 无需声明
export default async function ChatPage() {
const chats = await db.query.chats.findMany() // 直接查数据库
return <ChatList chats={chats} />
}
// 必须显式声明 'use client' 才是 Client Component
'use client'
export function ChatInput() {
const [text, setText] = useState('') // 可以用 Hooks
return <input value={text} onChange={(e) => setText(e.target.value)} />
}2.2 能力对比
| 能力 | Server Component | Client Component |
|---|---|---|
async/await | ✅ | ❌ |
| 数据库/文件系统访问 | ✅ | ❌ |
useState / useEffect | ❌ | ✅ |
事件处理 onClick | ❌ | ✅ |
浏览器 API(window/document) | ❌ | ✅ |
ref / DOM 操作 | ❌ | ✅ |
| 导入 Server Components | ✅ | ❌(但可以通过 children 传入) |
| 导入 Client Components | ✅ | ✅ |
| 发送 JS 到浏览器 | ❌(零 JS) | ✅ |
2.3 决策流程图
这个组件需要...?
├── 直接获取数据(数据库/API)
│ └── Server Component ✅
├── 使用 useState / useEffect / useRef
│ └── Client Component 'use client'
├── 事件处理(onClick / onChange)
│ └── Client Component 'use client'
├── 浏览器 API(window / localStorage)
│ └── Client Component 'use client'
├── 纯展示(接收 props 显示内容)
│ └── Server Component ✅(默认,JS 更小)
└── 不确定?
└── Server Component ✅(默认,需要时再改)
3. Server/Client 边界
3.1 'use client' 边界
'use client' 不仅仅影响当前文件——它定义了一条边界:
Server 域 Client 域
┌──────────────┐ ┌──────────────┐
│ page.tsx │ │ │
│ (Server) │ ─imports→ │ ChatInput │
│ │ │ 'use client' │
│ layout.tsx │ │ │
│ (Server) │ │ ChatUI │
│ │ │ (被拖入Client)│
└──────────────┘ └──────────────┘
规则:
1. Server Component 可以 import Client Component ✅
2. Client Component 不能 import Server Component ❌
3. Client Component import 的组件自动变为 Client Component
// 'use client' 是一条边界线
'use client'
// 这个文件中 import 的所有组件都会变成 Client Component
import { Button } from './button' // Button 变成 Client
import { formatDate } from './utils' // utils 的 JS 会被发送到浏览器
export function ChatInput() {
return <Button>Send</Button>
}3.2 常见错误
// ❌ 错误:在 Client Component 中导入 Server Component
'use client'
import { ServerOnlyComponent } from './server-component'
// 编译报错或运行时错误
// ❌ 错误:在 Client Component 中直接查数据库
'use client'
export function ChatList() {
const chats = await db.query.chats.findMany() // ❌ 无法访问数据库
}3.3 组合模式:children 模式
如何在 Client Component 中使用 Server Component?——通过 children 传入:
// app/chat/page.tsx (Server Component)
import { ChatLayout } from './chat-layout' // Client
import { ChatMessages } from './chat-messages' // Server
export default async function ChatPage() {
const messages = await getMessages() // 服务端获取数据
return (
<ChatLayout>
{/* ChatMessages 是 Server Component,但通过 children 传入 Client Component */}
<ChatMessages messages={messages} />
</ChatLayout>
)
}// chat-layout.tsx (Client Component)
'use client'
import { useState } from 'react'
export function ChatLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true)
return (
<div className="flex">
<Sidebar open={sidebarOpen} />
<main>{children}</main> {/* Server Component 通过 children 渲染 */}
</div>
)
}// chat-messages.tsx (Server Component — 不需要 'use client')
export async function ChatMessages({ messages }: { messages: Message[] }) {
return (
<ul>
{messages.map(m => <li key={m.id}>{m.content}</li>)}
</ul>
)
}3.4 组合模式总结
| 模式 | 代码 | 说明 |
|---|---|---|
| Server → Client | <ClientComp /> | Server 直接导入 Client ✅ |
| Client → Server (children) | <ClientComp>{serverContent}</ClientComp> | 通过 children/props 传入 ✅ |
| Client → Server (import) | import ServerComp | ❌ 不允许 |
4. 序列化规则
4.1 什么可以从 Server 传到 Client
Server Component 的渲染结果和 props 需要序列化后通过网络传输到浏览器。以下是可序列化的类型:
// ✅ 可序列化(可作为 Client Component 的 props)
string, number, boolean, null, undefined
bigint
Date
Array, Map, Set
Plain objects (字面量对象)
Server Components (JSX)
Promises(用于 Suspense)
// ❌ 不可序列化(不能作为 Client Component 的 props)
Functions (函数)
Classes (类实例)
Symbols
DOM nodes
Closures (闭包)4.2 实际影响
// Server Component
export default async function ChatPage() {
const chat = await getChat()
return (
<ChatClient
title={chat.title} // ✅ string
messageCount={chat.count} // ✅ number
createdAt={chat.createdAt} // ✅ Date
messages={chat.messages} // ✅ Array of plain objects
onDelete={deleteChat} // ❌ 函数不可序列化!
/>
)
}如何传递"行为"?——使用 Server Actions:
// Server Component
import { deleteChat } from './actions'
export default async function ChatPage() {
return (
<ChatClient
chatId={chat.id}
deleteChatAction={deleteChat} // ✅ Server Action 可序列化
/>
)
}
// actions.ts
'use server'
export async function deleteChat(chatId: string) {
await db.delete(chats).where(eq(chats.id, chatId))
revalidatePath('/chat')
}
// Client Component
'use client'
export function ChatClient({ chatId, deleteChatAction }: Props) {
return (
<button onClick={() => deleteChatAction(chatId)}>
删除
</button>
)
}5. RSC Payload
5.1 什么是 RSC Payload
RSC Payload 是 Server Components 渲染结果的序列化格式——一种紧凑的、可流式传输的二进制格式:
RSC Payload 包含:
├── Server Components 的渲染结果(类似虚拟 DOM)
├── Client Components 的 JS Bundle 引用(不是代码本身)
├── 从 Server Components 传给 Client Components 的 Props
└── Suspense 边界的状态(pending / resolved)
5.2 查看 RSC Payload
在浏览器 DevTools → Network Tab → 过滤 RSC 或 Fetch/XHR 请求,可以看到客户端导航时的 RSC Payload:
请求:GET /chat?_rsc=xxxxx
响应(RSC Payload,简化示意):
0:["$","div",null,{"className":"flex","children":[
["$","$Lsidebar",null,{}],
["$","main",null,{"children":"$L1"}]
]}]
1:["$","div",null,{"children":[
["$","h1",null,{"children":"对话列表"}],
["$","ul",null,{"children":[...]}]
]}]
$L前缀表示 Client Component 引用(Lazy load)- 数字前缀(
0:,1:)表示流式传输的 chunk
5.3 Payload 大小优化
减少 RSC Payload 的方法:
├── 只传必要的 props(不要传整个对象,只传需要的字段)
├── 使用分页/虚拟滚动(不要一次传 1000 条记录)
├── 大型数据用 Client 端请求(TanStack Query)
└── 图片/文件 URL 代替 Base64
// ❌ 传递过多数据
<ChatClient chat={fullChatObject} /> // 包含 messages、metadata、relations...
// ✅ 只传需要的字段
<ChatClient
title={chat.title}
id={chat.id}
messageCount={chat.messages.length}
/>6. 实战模式
6.1 AI SaaS 典型组件树
app/(dashboard)/chat/[id]/page.tsx (Server)
├── ChatLayout (Client — 需要侧边栏交互)
│ ├── Sidebar (Client — 折叠/展开)
│ │ └── ChatList (Server — 查询对话列表) ← 通过 children 传入
│ └── Main (Client — 滚动行为)
│ ├── ChatHeader (Server — 查询对话标题/模型)
│ ├── MessageList (Server — 查询消息列表)
│ │ ├── Message (Server — 纯展示)
│ │ └── Message (Server — 纯展示)
│ └── ChatInput (Client — 输入框/发送按钮)
6.2 最佳实践清单
1. 默认 Server Component,只在需要交互时用 'use client'
2. 'use client' 边界尽量下推(叶子组件)
3. 通过 children 模式组合 Server 和 Client
4. 用 Server Actions 代替函数 props
5. 只序列化必要的数据
6. 重型库(markdown 渲染、代码高亮)优先在 Server Component 中使用
6.3 第三方库兼容性
// 问题:很多第三方库不导出 'use client'
// 解决:创建包装文件
// components/client/recharts.tsx
'use client'
export { BarChart, Bar, XAxis, YAxis, Tooltip } from 'recharts'
// 然后在 Server Component 中使用
import { BarChart, Bar } from '@/components/client/recharts'本章小结
- RSC ≠ SSR:RSC 不发送 JS 到浏览器,SSR 发送全部 JS
- 默认 Server Component:只在需要 useState/onClick/浏览器 API 时添加
'use client' - 'use client' 是边界:它之后 import 的所有模块都变成 Client
- children 模式:在 Client Component 中使用 Server Component 的唯一方式
- 序列化规则:函数不可传,用 Server Actions 代替
- RSC Payload:Server Components 渲染结果的序列化格式,可流式传输
下一章讲 SSR/SSG/ISR 渲染策略选择。