正在加载,请稍候…

React Server Components 深入解析:RSC 底层工作原理

从基本原理理解 React Server Components:RSC 载荷、模块图、水合、Suspense 集成,以及 RSC 如何改变我们对 React 渲

React Server Components 深入解析:RSC 底层工作原理

RSC 解决的问题(我们未曾意识到的问题)

在 React Server Components 出现之前,React 存在一个根本性的矛盾:组件模型鼓励将数据获取与组件放在一起,但在客户端这样做意味着每个组件都会触发自己的网络请求,形成瀑布模式。

旧的解决方案——Redux 全局状态、数据获取库,或者将所有内容移到 getServerSideProps——都破坏了组件共置。最终数据获取与使用它的组件分离。

RSC 通过让组件本身在服务器上运行来解决这个问题,直接获取数据,而客户端完全不知道发生了网络请求。

React Server Components 深入解析: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 ]} ]}

这种格式:

  1. 可流式传输:块通过 HTTP 流式传输逐步到达
  2. 可差异比较:React 可以合并更新而无需完全重新加载页面
  3. 引用 Client Components:Server Components 不打包客户端代码——它们引用它
  4. 携带序列化的 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

React Server Components 深入解析:RSC 底层工作原理 插图

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 是特殊的)

React Server Components 深入解析:RSC 底层工作原理 插图

客户端 → 服务器(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 和 Context
  • useActionState():替代 useFormState 用于 Server Actions
  • useOptimistic():带有自动回滚的乐观更新
  • 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 数据结构。