Skip to main content

React Server Components(RSC)深度解析

面试题:什么是 RSC?和 SSR 有什么本质区别?什么场景下用?

一、为什么需要 RSC?

1.1 传统 SSR 的三大问题

传统 SSR 流程:
浏览器 ──→ 服务器 ──→ renderToString(<App />) ──→ HTML

所有组件代码
全部 hydrate
全部发到浏览器

即使是「无交互」组件

问题清单

  1. bundle 过大:所有组件 JS 都要发给浏览器
  2. 数据获取链路长:组件需要 API 间接拿数据
  3. hydrate 必须全量:哪怕是纯展示组件也要 hydrate

1.2 RSC 的核心解决思路

把组件分成两类

  • Server Component:服务端跑完,代码不发给浏览器
  • Client Component:传统 React,代码发给浏览器、hydrate
RSC 流程:
浏览器 ──→ 服务器
├─ Server Component 跑完 → 不发 JS
├─ Client Component 标记位置 + 发 JS
└─ 一起返回(特殊格式,叫 RSC Payload)

浏览器 ──→ 解析 RSC Payload
──→ 渲染 HTML
──→ 只 hydrate Client Component

二、RSC 与 SSR 的本质区别

维度SSRRSC
HTML 来源服务端 renderToString服务端跑组件 → RSC Payload → 客户端解析
JS 下发全部组件 JS只有 Client Component 的 JS
Hydration全量 hydrate只 hydrate Client Component
数据获取API 中转组件内 await db.xxx() 直连
bundle 体积整个应用可能减少 30%~70%
首屏可见快(HTML 先到)更快(HTML + 部分 JS 并行)

面试一句话区分SSR 把组件代码发给浏览器再 hydrate;RSC 把组件渲染结果发给浏览器,浏览器只 hydrate 需要交互的部分。

三、RSC Payload 协议

3.1 是什么?

RSC 用一种类似 JSON 的特殊序列化格式传输服务端组件树。它包含:

  • 组件树结构
  • 渲染结果(HTML 字符串)
  • 传给 Client Component 的 props(已序列化)

3.2 长什么样?

{
"$$typeof": "react.element",
"type": "div",
"key": null,
"ref": null,
"props": {
"children": [
{
"type": "h1",
"props": { "children": "文章列表" }
},
{
"type": "ClientButton", // ← Client Component 引用
"props": { "id": 1 },
"$$typeof": "react.client.reference"
}
]
}
}

关键点:Server Component 在 Payload 里是真实 DOM 结构(已渲染完),Client Component 是引用标记(等 JS 到了再实例化)。

3.3 为什么要用这种格式?

  1. 保留组件树结构:浏览器能识别嵌套关系
  2. 数据已就位:Client Component 拿到的不再是「API 引用」而是「真实数据」
  3. 可流式传输:能配合 Suspense 边渲染边发

四、Server Component vs Client Component

4.1 规则

维度Server ComponentClient Component
声明方式默认文件顶部 "use client"
能用什么await、DB、fs、敏感 APIuseState、事件、浏览器 API
不能用什么useState、事件、浏览器 API直连 DB、fs(安全)
打包服务端跑完即丢客户端 hydrate

4.2 边界规则

// ✅ Server Component 可以:
// 1. 渲染 Server Component
// 2. 渲染 Client Component(把 Server 数据当 props 传)
import { ClientButton } from "./ClientButton"; // 有 "use client"

export default async function Page() {
const data = await db.user.findMany(); // 服务端直连
return <ClientButton user={data} />; // ← 传 props 给 Client
}

// ❌ Client Component 不能:
// 1. import Server Component(会报错)
"use client";
import { ServerList } from "./ServerList"; // ❌ 报错

// 2. 在 Client Component 里 await 数据
"use client";
const data = await fetch(...); // ❌ 必须在事件里

4.3 共享组件("use client" 在子组件)

// Avatar.tsx —— 没 "use client",是「共享组件」
export function Avatar({ user }) {
return <img src={user.avatar} />;
}

// 父组件 A(Server)
import { Avatar } from "./Avatar"; // 当 Server Component 用
<Avatar user={...} />

// 父组件 B(Client)
"use client";
import { Avatar } from "./Avatar"; // 也可当 Client Component 用
<Avatar user={...} />

机制:未声明 "use client" 的组件是「通用组件」,可被两种父组件消费。React 会根据消费场景决定它最终跑在哪边。

4.4 错误示范

// ❌ Server Component 不能传给 Client Component 一个函数
async function ServerList() {
return <ClientList onClick={() => console.log("hi")} />; // 报错
}
// 因为 onClick 是函数,无法序列化进 RSC Payload

// ✅ 必须用 props 传数据,事件在 Client Component 内部绑定

五、Server Actions

面试题:Server Actions 是什么?和传统 API Route 什么区别?

5.1 本质

Server Actions 是在服务端执行的异步函数,可以在 Client Component 中像普通函数一样调用,无需写 fetch / API route。

5.2 用法

// app/actions/post.ts
"use server";

export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
revalidatePath("/posts");
}
// app/components/NewPostForm.tsx
"use client"; // 需要交互
import { createPost } from "@/app/actions/post";

export function NewPostForm() {
return (
<form action={createPost}> {/* ← 直接传函数 */}
<input name="title" />
<button type="submit">发布</button>
</form>
);
}

5.3 工作原理

Client Component 调用 createPost()

React 编码成一个特殊的 form action

浏览器发起 POST /xxx(带 action ID)

服务器识别 action ID → 找到对应函数 → 执行

返回结果(包含 revalidate 指令)

Client 自动重新渲染

5.4 与 API Route 的对比

维度API RouteServer Actions
写在哪app/api/.../route.ts任何文件,配 "use server"
调用方式客户端 fetch直接当函数调
参数手动解析(JSON、FormData)自动序列化
安全自己加鉴权自动加密 action ID
CSRF需手动处理内置保护(只接受 POST)
进度反馈自己写 loading 状态useFormStatus 一行解决

5.5 useActionState(React 19)

"use client";
import { useActionState } from "react";

async function createUser(prevState, formData) {
const name = formData.get("name");
if (!name) return { error: "姓名不能为空" };
await db.user.create({ data: { name } });
return { success: true };
}

export function Form() {
const [state, action, isPending] = useActionState(createUser, { error: null });

return (
<form action={action}>
<input name="name" />
<button disabled={isPending}>提交</button>
{state.error && <p style={{ color: "red" }}>{state.error}</p>}
</form>
);
}

六、为什么 RSC 能让 bundle 变小?

6.1 一个案例

// app/posts/page.tsx —— Server Component
import { marked } from "marked"; // 100KB+ 库
import { db } from "@/lib/db";
import { LikeButton } from "./LikeButton"; // Client

export default async function PostsPage() {
const posts = await db.post.findMany();
return (
<div>
{posts.map(p => (
<article key={p.id}>
<h1>{p.title}</h1>
<div dangerouslySetInnerHTML={{ __html: marked(p.content) }} />
<LikeButton postId={p.id} />
</article>
))}
</div>
);
}

bundle 体积

组件浏览器收到 JS原因
marked0 KBServer 跑完就丢
db(数据库连接)0 KB不能暴露给客户端
PostsPage 组件0 KBServer 跑完就丢
LikeButton5 KB唯一需要交互的

传统 SSR:所有上述 JS 都会发到浏览器 → 110KB+ RSC:只发 LikeButton 的 5KB → 减少了 95%

七、RSC 的限制

7.1 不能在 Server Component 用

  • useState / useReducer / useEffect 等 Hooks(除了 useId
  • ❌ 事件处理(onClickonChange
  • ❌ 浏览器 API(windowlocalStorage
  • Date.now() / Math.random()(会和客户端不一致)

7.2 不能传给 Client Component

  • ❌ 函数(不可序列化)
  • ❌ Date 对象(要转 ISO 字符串)
  • ❌ Map / Set(要转 plain object)
  • ❌ class 实例(要转 plain object)

八、RSC + SSR + Streaming 三者结合

Next.js 15 的全栈渲染模型

请求 ──→ Server

├─ 跑所有 Server Components
│ ├─ 直连 DB
│ ├─ 拼成 RSC Payload
│ └─ 遇到 Client Component → 标记边界

├─ 通过 renderToPipeableStream 发出
│ ├─ 头部 / 已就绪部分立刻推流
│ └─ Suspense 兜底部分等数据再推

└─ 浏览器
├─ 收到 RSC Payload → 解析 → 显示 HTML
├─ 下载 Client Component JS → 局部 hydrate
└─ hydrate 完成 → 完整可交互

九、面试高频追问

Q1:RSC 是 React 18 还是 19 的特性?

A:React 18 引入,Next.js 13+ 落地。React 19 进一步增强了 Server Actions、useActionState 等配套 API。

Q2:RSC 必须用 Next.js 吗?

A:理论不绑定,但目前只有 Next.js App Router 提供了完整生产实现。其他框架(Remix、RedwoodJS)有自己的服务端渲染方案但不是 RSC 协议。

Q3:RSC 和 Server Actions 什么关系?

A:RSC 是「服务端能跑组件」的能力;Server Actions 是「服务端能跑函数」的能力。两者都基于 "use server" / "use client" 边界,但作用对象不同(一个渲染组件,一个处理数据变更)。

Q4:为什么说 RSC 是「服务端能直连 DB」?

A:因为代码不发给浏览器,DB 凭证不会泄露;RSC 跑在服务端,有访问 DB 的网络能力。这是个安全 + 架构上的革命

Q5:RSC 的 trade-off 是什么?

A:

  • 失去了一些 Client 灵活性(如不能直接 onClick)
  • 跨组件通信更复杂(Context 不能跨 RSC 边界)
  • 调试工具还在迭代

十、相关文档