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

这是最常见的误解:

维度SSRRSC
运行位置服务端渲染 → 发送 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 ComponentClient 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&lt;ClientComp /&gt;Server 直接导入 Client ✅
Client → Server (children)&lt;ClientComp&gt;&#123;serverContent&#125;&lt;/ClientComp&gt;通过 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 → 过滤 RSCFetch/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 渲染策略选择。