一句话定义
Server Component 是只在服务端运行、不进客户端 bundle、渲染结果以序列化流的形式发给客户端的组件;与之相对,Client Component('use client')拥有状态与浏览器 API。两者可以在同一棵树里穿插组合。
为什么重要
RSC 改变了「React 组件默认在哪里执行」:数据获取移回服务端(消除客户端瀑布请求)、重型依赖不再打进 JS 包、首屏从「等 JS 加载」变成「流式送达」。这是 2024 年以来前端架构最大的范式迁移,Next.js App Router、RedwoodJS 等均以它为地基。
前置知识
核心概念
- 默认即 Server:Next.js App Router 中组件默认是 Server Component;需要交互才标
'use client'。 - 能力边界:Server 端有——直连数据库/文件/密钥、async/await 取数;没有——useState/useEffect/useRef、事件处理器、浏览器 API。
- Client Component 不是「服务端渲染的组件」:它也会在服务端做 SSR(产出 HTML),但之后还要在浏览器 hydrate 并常驻运行;RSC 的输出则是一次性的序列化树。
- props 序列化规则:跨边界只能传「可序列化」的值(字符串/数字/纯对象/数组);函数、类实例不可传;children 可以——因为传的是「已渲染的服务端产物」。
- 流式 + Suspense:服务端逐块发送已就绪的 UI,配合客户端 Suspense 边界渐进显示。
原理 / 机制
一棵混合树与数据流:
jsx// page.jsx —— Server Component(默认)
import { db } from './db';
import LikeButton from './LikeButton';
export default async function PostPage({ params }) {
const post = await db.post.find(params.id); // 直接查库,无需 API 中转
const related = await db.post.related(post); // 两个请求并行/按需
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
{/* Server → Client 边界:只能传可序列化 props */}
<LikeButton postId={post.id} initialCount={post.likes} />
{/* children 穿越边界:传的是"已渲染的 Server UI" */}
<Sidebar>{await renderRelated(related)}</Sidebar>
</article>
);
}
// LikeButton.jsx —— Client Component
'use client';
import { useState, useOptimistic } from 'react';
export default function LikeButton({ postId, initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>👍 {count}</button>;
}
渲染管线:
text请求 → 服务端执行 Server Components(可 await 数据)
→ 产出 RSC 载荷(序列化树,含 Client Component 的"占位引用")
→ 流式发送:HTML 骨架 + RSC 载荷
→ 客户端按 Suspense 边界渐进显示;Client Component hydrate
→ 后续导航:只更新 RSC 载荷,不刷新整页
收益对照(同一个「产品列表页」):
| 维度 | 纯 SPA | RSC |
|---|---|---|
| 数据获取 | 浏览器先载 JS → 再发 API → 再渲染(瀑布) | 服务端取数随 HTML 一起送达 |
| 首屏 JS | 全部组件与依赖进 bundle | Server 组件 0 KB,只有交互部分进包 |
| markdown/语法高亮等重库 | 打进客户端 | 只在服务端运行,不进包 |
| SEO | 需 SSR/SSG 配置 | 天然 HTML |
直观类比
SPA 像宜家自提:网页先下载整套说明书和零件(JS bundle),你在家自己组装(客户端渲染)。SSR 像快递半成品家具:到货就能用,但你还得自己拧最后一遍螺丝(hydrate)。RSC 像全屋定制:工厂(服务端)直接把装好的柜子(序列化 UI)送上门,你只需对「带电的部分」(交互组件)做接线——电钻(交互 JS)只买真正需要的那几件。
实例 / 案例
「Server 拉数据 + Client 处理交互」的搜索页:
jsx// app/search/page.jsx(Server)
export default async function SearchPage({ searchParams }) {
const results = await searchProducts(searchParams.q); // 服务端取数
return <SearchResults items={results} />;
}
// components/SearchBox.jsx(Client)
'use client';
export default function SearchBox({ initial }) {
const router = useRouter(); // 交互在客户端
const [q, setQ] = useState(initial);
return (
<form onSubmit={e => {
e.preventDefault();
router.push(`/search?q=${encodeURIComponent(q)}`); // 回到服务端取新数据
}}>
<input value={q} onChange={e => setQ(e.target.value)} />
</form>
);
}
注意心智转变:交互组件不再自己 fetch,而是「把查询参数写回 URL」,由服务端重新渲染——单向数据流上升到了「URL 即状态」的层面。
常见误区
- 「RSC 就是 SSR」:SSR 是「服务端产出首屏 HTML + 客户端 hydrate 全部组件」;RSC 是「组件常驻服务端,产物序列化下发」。两者可以叠加。
- 给 Server Component 加 onClick/useState:直接报错或被静默忽略;交互部分拆成
'use client'子组件。 - 跨边界传函数/复杂对象:序列化失败;传 id 让子组件自取,或用 Actions(kp-025)传递「可调用的服务端函数引用」。
- 把整个页面标 'use client':边界应尽量下沉到「叶子交互组件」,让数据拉取与静态部分留在服务端。
- 忘记 Client Component 也做 SSR:以为 'use client' 意味着「不服务端渲染」——它只是意味着「会在客户端运行/有状态」。
自测题
- 列出 Server Component 能做与不能做的各三件事。
- 「'use client' 的组件不做 SSR」错在哪?
- 为什么 children 能跨越 RSC 边界而函数不能?用序列化解释。
- 把「产品详情页」拆成 Server/Client 两部分的划分依据是什么?给出你的组件清单。
- RSC 相对纯 SPA 的两个数量级收益是什么?
与其他知识点的关系
- 向前:组合模式(kp-013)与 Suspense(kp-020)是它的语法基础。
- 向后:kp-025 的 Server Actions 是 RSC 体系的「上行通道」;kp-026 是它的落地框架;kp-019 的首屏优化问题在 RSC 下被部分结构性解决。
延伸阅读
- Server Components(官方 RSC 指南):<https://react.dev/reference/rsc/server-components>
- Next.js Server Components:<https://nextjs.org/docs/app/building-your-application/rendering/server-components>
- RSC 解释文(Dan Abramov):<https://github.com/reactwg/server-components/discussions/5>