React Server Components(RSC)深度解析
面试题:什么是 RSC?和 SSR 有什么本质区别?什么场景下用?
一、为什么需要 RSC?
1.1 传统 SSR 的三大问题
传统 SSR 流程:
浏览器 ──→ 服务器 ──→ renderToString(<App />) ──→ HTML
↑
所有组件代码
全部 hydrate
全部发到浏览器
↓
即使是「无交互」组件
问题清单:
- bundle 过大:所有组件 JS 都要发给浏览器
- 数据获取链路长:组件需要 API 间接拿数据
- 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 的本质区别
| 维度 | SSR | RSC |
|---|---|---|
| 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 为什么要用这种格式?
- 保留组件树结构:浏览器能识别嵌套关系
- 数据已就位:Client Component 拿到的不再是「API 引用」而是「真实数据」
- 可流式传输:能配合 Suspense 边渲染边发
四、Server Component vs Client Component
4.1 规则
| 维度 | Server Component | Client Component |
|---|---|---|
| 声明方式 | 默认 | 文件顶部 "use client" |
| 能用什么 | await、DB、fs、敏感 API | useState、事件、浏览器 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 Route | Server 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 | 原因 |
|---|---|---|
marked | 0 KB | Server 跑完就丢 |
db(数据库连接) | 0 KB | 不能暴露给客户端 |
PostsPage 组件 | 0 KB | Server 跑完就丢 |
LikeButton | 5 KB | 唯一需要交互的 |
传统 SSR:所有上述 JS 都会发到浏览器 → 110KB+ RSC:只发 LikeButton 的 5KB → 减少了 95%
七、RSC 的限制
7.1 不能在 Server Component 用
- ❌
useState/useReducer/useEffect等 Hooks(除了useId) - ❌ 事件处理(
onClick、onChange) - ❌ 浏览器 API(
window、localStorage) - ❌
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 边界)
- 调试工具还在迭代