
RSC 解决的问题(我们未曾意识到的问题)
在 React Server Components 出现之前,React 存在一个根本性的矛盾:组件模型鼓励将数据获取与组件放在一起,但在客户端这样做意味着每个组件都会触发自己的网络请求,形成瀑布模式。
旧的解决方案——Redux 全局状态、数据获取库,或者将所有内容移到 getServerSideProps——都破坏了组件共置。最终数据获取与使用它的组件分离。
RSC 通过让组件本身在服务器上运行来解决这个问题,直接获取数据,而客户端完全不知道发生了网络请求。

RSC 不是什么
首先,让我们澄清一些混淆:
- RSC ≠ SSR:服务端渲染(SSR)在服务器上将 React 渲染为 HTML,然后在客户端进行水合。每个组件仍然在客户端运行。RSC 组件从不在客户端运行。
- RSC ≠ SSG:静态站点生成在构建时生成 HTML。RSC 可以在请求时、构建时或缓存时获取数据。
- RSC ≠ 仅仅是“没有 useState”:区别在于执行环境,而不是能力。Server Components 在 Node.js(或 Edge)中运行,而不是在浏览器中。
RSC 线格式
当 React Server Component 树渲染时,它不会直接产生 HTML。它产生一种特殊的序列化格式——通常称为“RSC 载荷”或“RSC 线格式”:
// RSC 实际通过网络发送的内容(简化)
1:{"id":"app/page.tsx","chunks":["main"],"name":""}
2:I{"id":"app/Counter.tsx","chunks":["counter"],"name":"Counter"} // "I" = Client Component 引用
3:{"children":["quot;,"div",null,{"children":[
["quot;,"h1",null,{"children":"Hello from Server"}],
["quot;,"$2",null,{"initialCount":5}] // 带 props 的 Client Component
]}
]}
这种格式:
- 可流式传输:块通过 HTTP 流式传输逐步到达
- 可差异比较:React 可以合并更新而无需完全重新加载页面
- 引用 Client Components:Server Components 不打包客户端代码——它们引用它
- 携带序列化的 props:数据以 JSON 形式从服务器流向客户端
模块图分割
RSC 中最重要的架构概念是模块图分割:
Server Module Graph Client Module Graph
──────────────────── ──────────────────
app/page.tsx app/Counter.tsx
├── db/queries.ts ├── react
├── lib/auth.ts ├── react-dom
├── app/UserCard.tsx └── components/Chart.tsx
│ └── lib/format.ts
└── [reference] Counter.tsx ──────► (boundary)
Server Components 及其依赖项永远不会包含在客户端包中。这具有巨大的影响:
// 这段代码在服务器上运行——客户端永远不会下载:
import { marked } from 'marked' // 27KB
import { highlight } from 'highlight.js' // 180KB
import { parse } from 'some-parser' // 45KB
export default async function Article({ slug }: { slug: string }) {
const content = await db.getArticleContent(slug)
const html = marked(highlight(content)) // 零客户端包影响
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
与传统的做法相比,这些库会膨胀客户端包。
渲染生命周期
以下是请求到达 Next.js App Router 页面时发生的情况:
1. 请求到达 Next.js 服务器
↓
2. Next.js 识别路由 → app/blog/[slug]/page.tsx
↓
3. React 开始渲染 Server Components
- 执行异步操作(数据库查询、fetch 调用)
- 解析 RSC 树
↓
4. RSC 载荷流式传输到客户端
- 立即发送 HTML 外壳(以实现快速 FCP)
- RSC 块在解析时流式传输
↓
5. 客户端接收 RSC 载荷
- React 与服务器渲染的 HTML 进行协调
- 下载并水合 Client Component 包
↓
6. 页面可交互
- Client Components 水合(事件处理程序附加)
- Server Components 是“静态的”——没有客户端 JS

Suspense 与流式传输
Suspense 是使 RSC 流式传输工作的机制:
// app/dashboard/page.tsx
import { Suspense } from 'react'
// 每个异步 Server Component 都是一个潜在的 Suspense 边界
async function SlowComponent() {
await new Promise(r => setTimeout(r, 2000)) // 模拟慢速数据库查询
const data = await fetchExpensiveData()
return <DataDisplay data={data} />
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 立即渲染 */}
<FastStats />
{/* 当 SlowComponent 解析时流式传输 */}
<Suspense fallback={<div>Loading analytics...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
HTTP 响应中实际发生的情况:
<!-- 立即:外壳 + 加载状态 -->
<div>
<h1>Dashboard</h1>
<div>Loading analytics...</div> <!-- Fallback -->
</div>
<!-- 2 秒后,在同一个 HTTP 响应中流式传输: -->
<div hidden id="S:1">
<!-- SlowComponent 输出 -->
<div class="analytics">...</div>
</div>
<script>
// React 用实际内容替换 fallback
$RC("B:1", "S:1")
</script>
关键:这是一个 HTTP 请求。响应保持打开状态,块在准备好时被推送。无需轮询,无需客户端获取初始数据。
“use client”指令
'use client' // 这是一个模块边界声明,而不是编译指示
“use client”告诉 React 打包工具:“此模块及其导入的所有内容都应包含在客户端包中。”它在服务器和客户端模块图之间创建了一个边界。
误解:许多开发者认为“use client”意味着组件仅在客户端运行。事实并非如此——Client Components 仍然进行 SSR!“use client”意味着组件在服务器(SSR 期间)和客户端(用于水合和重新渲染)上都运行。
// 错误的思维模型:
'use client' // “此代码仅在浏览器中运行”
// 正确的思维模型:
'use client' // “这是一个 Client Component:
// 1. 包含在客户端包中
// 2. 在服务器上 SSR 为 HTML
// 3. 在客户端水合和运行
// 4. 在客户端重新渲染以更新状态/效果”
数据传递模式
服务器 → 客户端(Props)
// Server Component
async function ServerParent() {
const user = await db.getUser(1) // { id: 1, name: 'Alice', role: 'admin' }
return <ClientChild user={user} /> // 在 RSC 载荷中序列化为 JSON
}
// Props 必须是可序列化的:
// ✓ string, number, boolean, null, undefined
// ✓ 普通对象和数组
// ✓ Date(序列化为 ISO 字符串——注意!)
// ✗ 函数
// ✗ 类实例
// ✗ React 元素(但 children prop 是特殊的)

客户端 → 服务器(Server Actions)
'use server'
// Server Action——从 Client Component 调用但在服务器上运行
export async function updateUser(userId: number, data: { name: string }) {
// 在服务器上运行——可以访问数据库、环境变量等
const updated = await db.updateUser(userId, data)
revalidatePath('/profile')
return updated
}
'use client'
import { updateUser } from './actions'
function ProfileForm({ userId }: { userId: number }) {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const formData = new FormData(e.target as HTMLFormElement)
// 从 Client Component 调用 Server Action
// 自动向服务器发起 HTTP POST 请求
await updateUser(userId, { name: formData.get('name') as string })
}
return <form onSubmit={handleSubmit}>...</form>
}
Context 与共享状态
React Context 在 Server Components 中不起作用(服务器上没有 React 树状态)。以下是共享数据的模式:
// 模式 1:作为 props 传递(最简单)
async function Layout({ children }: { children: React.ReactNode }) {
const user = await getUser() // 服务器端认证
return <AppShell user={user}>{children}</AppShell>
}
// 模式 2:通过 cookies/headers 实现服务器端“context”
import { cookies } from 'next/headers'
async function ThemeLayout({ children }: { children: React.ReactNode }) {
const theme = cookies().get('theme')?.value ?? 'light'
return (
<div data-theme={theme}>
{children}
</div>
)
}
// 模式 3:用于交互状态的 Client Context
'use client'
const UserContext = createContext<User | null>(null)
export function UserProvider({ user, children }: { user: User, children: React.ReactNode }) {
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
}
// 在根布局中使用(Server Component):
async function RootLayout({ children }: { children: React.ReactNode }) {
const user = await getUser()
return (
<html>
<body>
<UserProvider user={user}> {/* Client Component 包裹 */}
{children}
</UserProvider>
</body>
</html>
)
}
RSC 中的缓存
Next.js 的缓存是分层的,并在多个级别上运行:
Request Memoization — 同一 render 中重复的 fetch() 调用被去重
Data Cache — fetch() 结果跨请求缓存(持久化)
Full Route Cache — 构建时缓存的渲染后的 RSC 输出
Router Cache — 客户端缓存的 RSC 载荷(默认 30 秒)
// 不同缓存策略的 fetch():
const data1 = await fetch(url) // 无限期缓存
const data2 = await fetch(url, { cache: 'no-store' }) // 从不缓存
const data3 = await fetch(url, { next: { revalidate: 60 } }) // 60 秒 TTL
// 请求记忆化——同一 URL 在同一个 render 中被获取两次 = 一次实际请求
async function getUser(id: string) {
const res = await fetch(`/api/users/${id}`) // React 记忆化此调用
return res.json()
}
// 这两个都调用 getUser——只发起一个 HTTP 请求:
async function Profile({ userId }: { userId: string }) {
const user = await getUser(userId) // 缓存在 React 的请求作用域中
return <div>{user.name}</div>
}
async function Avatar({ userId }: { userId: string }) {
const user = await getUser(userId) // 返回相同的记忆化结果
return <img src={user.avatar} />
}
何时不使用 Server Components
Server Components 并不总是正确的选择:
- 高交互性 UI:具有实时验证的表单、拖放界面——这些本质上需要客户端状态
- 浏览器特定功能:WebSocket、WebRTC、localStorage、地理位置
- 动画库:Framer Motion、GSAP——这些需要客户端生命周期
- 第三方组件:大多数现有的 React 组件库都是 Client Components
经验法则:尽可能将内容推送到 Server Components,在最小的边界添加“use client”。
React 19 与 RSC
React 19(2024 年发布)稳定了 RSC API 并添加了:
use()钩子:可以在 Client Components 中解包 Promise 和 ContextuseActionState():替代useFormState用于 Server ActionsuseOptimistic():带有自动回滚的乐观更新- Form Actions:Client Components 中原生的表单操作支持
'use client'
import { use, Suspense } from 'react'
// React 19 新特性:use() 可以在 Client Components 中解包 Promise
function UserName({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // 挂起直到解析
return <span>{user.name}</span>
}
// 将 Promise 从 Server 传递给 Client Component
async function ServerComponent() {
const userPromise = fetchUser(1) // 不要 await——直接传递 Promise
return (
<Suspense fallback={<span>Loading...</span>}>
<UserName userPromise={userPromise} />
</Suspense>
)
}
RSC 代表了 React 历史上最重大的转变之一。这种思维模型需要时间来内化,但性能和开发者体验上的好处——特别是消除了 API 样板代码和瀑布式数据获取——使得深入理解它非常值得。
→ 使用 JSON Viewer 可视化和探索 JSON 数据结构。