React 服务端组件
概述
React Server Components(RSC,服务端组件)是 React 18 和 Next.js 13 App Router 引入的最重要的新架构。它解决了一个根本性问题:客户端(浏览器)和服务端之间,应该如何分配 UI 渲染工作?
传统的 React SSR(Server-Side Rendering)方案(如 Next.js Pages Router)本质上是"在服务端渲染 HTML,然后在客户端激活(hydration)"。但这种方式的问题是:所有组件代码都必须打包发送到客户端,无论这个组件是否需要交互。
RSC 改变了这一点:某些组件可以永远只在服务端运行,它们的代码永远不会发送到客户端。这从根本上优化了 bundle 大小和首屏加载性能。
理解 RSC,不仅仅是学习一个新 API,更是理解 React 在"服务端能力"和"客户端能力"之间如何划分边界。
RSC 解决的问题
传统 SSR 的困境
考虑一个典型的博客文章页面:
// ArticlePage.jsx
function ArticlePage({ articleId }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetchComments(articleId).then(setComments);
}, [articleId]);
return (
<article>
<ArticleContent article={article} /> {/* 纯展示 */}
<AuthorBio author={author} /> {/* 纯展示 */}
<CommentSection comments={comments} /> {/* 需要交互 */}
</article>
);
}
问题:整个 ArticlePage 及其所有子组件都会被打包发送到客户端,包括那些永远不需要 JavaScript 的纯展示组件(ArticleContent、AuthorBio)。
在传统 SSR 模式下:
- 服务端渲染完整的 HTML
- 客户端下载包含 ArticlePage 所有代码的 JS bundle
- 客户端执行 hydration,让页面可交互
结果:用户下载了大量永远不需要执行的 JavaScript,只因为"也许未来某个时刻会需要交互"。
RSC 的解决思路
RSC 的核心洞察是:不是所有组件都需要在客户端运行。
// ArticlePage.tsx (Server Component - 默认)
async function ArticlePage({ articleId }) {
// 可以直接 await 数据库或 API
const article = await db.articles.find(articleId);
const author = await db.users.find(article.authorId);
return (
<article>
<ArticleContent article={article} /> {/* Server Component */}
<AuthorBio author={author} /> {/* Server Component */}
<CommentSection articleId={articleId} /> {/* Client Component */}
</article>
);
}
// CommentSection.tsx (Client Component)
'use client';
function CommentSection({ articleId }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetchComments(articleId).then(setComments);
}, [articleId]);
return (
<div>
{comments.map(c => <Comment key={c.id} text={c.text} />)}
<CommentForm onSubmit={submitComment} />
</div>
);
}
RSC 模式:
ArticlePage、ArticleContent、AuthorBio在服务端渲染,永不发送到客户端- 只有
CommentSection(需要交互)会被打包发送到客户端 - 服务端直接返回渲染后的 HTML,不需要完整 hydration
结果:
- JS bundle 大幅减小
- 首屏加载更快
- SEO 友好(HTML 完整)
Server Component vs Client Component
核心区别
| 特性 | Server Component | Client Component |
|---|---|---|
| 指令 | 无(默认) | 'use client' |
| 代码位置 | 服务端 | 客户端(但服务端也可渲染) |
| JS Bundle | 不包含 | 包含 |
| 可以使用 Hooks | ❌ | ✅ |
| 可以使用浏览器 API | ❌ | ✅ |
| 可以使用事件监听 | ❌ | ✅ |
| 支持 async/await | ✅ | ❌(在 render 中) |
| 直接访问数据库/文件系统 | ✅ | ❌ |
关键理解:客户端组件也能 SSR
重要:'use client' 并不意味着组件"只在客户端运行"。Client Component 仍然可以在服务端渲染(SSR),'use client' 只是表示:
"这个组件需要客户端 JavaScript 来实现完整交互能力"
'use client';
// 这个组件在 Next.js 中会:
// 1. 服务端渲染一次,生成 HTML
// 2. 客户端再渲染一次(hydration),绑定事件
function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(l => l + 1)}>
{likes} likes
</button>
);
}
边界原则
核心原则:将 'use client' 边界推到组件树的最低层。
// ✅ 正确:只在真正需要交互的地方用 'use client'
// ServerComponent.tsx
async function ServerComponent() {
const data = await fetchData();
return (
<div>
<ExpensiveChart data={data} /> {/* Server Component */}
<LikeButton initialLikes={100} /> {/* Client Component */}
</div>
);
}
// ❌ 错误:把整个父组件变成 Client Component
'use client';
async function ServerComponent() { // 错误:'use client' 和 async 不能共存
const data = await fetchData();
return <ExpensiveChart data={data} />;
}
常见的边界错误
错误 1:Client Component 嵌套 Server Component
'use client';
function ParentComponent() {
return (
<div>
<ServerChild /> {/* ❌ 错误:不能从 Client Component 导入 Server Component */}
</div>
);
}
为什么不行:Server Component 在客户端无法渲染(它需要服务端环境)。如果你在一个 Client Component 内部尝试使用 Server Component,客户端会尝试执行它,但 Server Component 可能需要数据库访问等服务端能力。
错误 2:Server Component 中使用 Hooks
// ❌ 错误:Server Component 不能使用 useState
function ServerComponent() {
const [count, setCount] = useState(0); // 编译错误
return <div>{count}</div>;
}
错误 3:Server Component 中使用 async 和 state 混合
// ❌ 错误
function ServerComponent() {
const [data, setData] = useState(null); // ❌ 不能用 useState
useEffect(() => { // ❌ 不能用 useEffect
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
Server Component 和 Client Component 的通信
Props 传递:数据序列化
Server Component 和 Client Component 之间只能通过 props 传递可序列化的数据。
// Server Component
async function Page() {
const data = await fetchData(); // data 必须是可序列化的
return (
<ClientComponent
// 这些 props 会序列化后传递
initialData={data} // ✅ plain object
userName={data.user.name} // ✅ string
createdAt={data.createdAt} // ✅ Date 会转为 string
/>
);
}
可序列化的类型:strings、numbers、booleans、arrays、objects、Date(转为 ISO string)、null。
不可序列化的类型:functions、class instances、React elements。
Server Component 的数据获取
Server Component 可以使用 async/await,这意味着数据获取可以直接在组件层完成:
// app/posts/page.tsx
async function PostsPage() {
// 直接在组件中 await,不需要 useEffect
const posts = await db.posts.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<main>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</main>
);
}
好处:
- 减少数据获取的样板代码
- 可以使用 await,而不用处理 Promise
- 便于 colocation——数据获取逻辑和组件放在一起
客户端组件接收数据
'use client';
// 接收序列化的数据作为 props
function PostList({ initialPosts }) {
const [posts, setPosts] = useState(initialPosts);
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
// 父组件(Server Component)
async function Page() {
const posts = await db.posts.findMany();
return <PostList initialPosts={posts} />; // 传递序列化后的数据
}
组合模式:服务端与客户端的协作
模式 1:Props 传递数据
最简单的方式:通过 props 将 Server Component 的数据传递给 Client Component。
// Server Component
async function Dashboard() {
const metrics = await fetchMetrics();
return (
<div>
<MetricsChart data={metrics} /> {/* Client Component */}
<LastUpdated time={metrics.updatedAt} />
</div>
);
}
// Client Component
'use client';
function MetricsChart({ data }) {
const canvasRef = useRef(null);
useEffect(() => {
// 用 Chart.js 绑定 canvas
const chart = new Chart(canvasRef.current, { data });
return () => chart.destroy();
}, [data]);
return <canvas ref={canvasRef} />;
}
模式 2:组合(Children Slot)
// Server Component
async function Dashboard({ children }) {
const sidebar = await fetchSidebarData();
return (
<div className="dashboard">
<aside>{children}</aside>
<main>
<Sidebar data={sidebar} />
</main>
</div>
);
}
// 父组件(Server Component)
async function Page() {
return (
<Dashboard>
<NotificationsPanel /> {/* 这是另一个 Server Component */}
</Dashboard>
);
}
模式 3:Context(仅限 Client Component)
Context 只能在 Client Component 中使用。如果 Server Component 需要共享数据,有几种方案:
方案 A:通过 props 层层传递
// Server Component
async function App({ userId }) {
const user = await fetchUser(userId);
return <MainLayout user={user}><PageContent /></MainLayout>;
}
// Client Component
'use client';
function MainLayout({ user, children }) {
return (
<UserContext.Provider value={user}>
<header>{user.name}</header>
<main>{children}</main>
</UserContext.Provider>
);
}
方案 B:用 Server Component 作为数据层
// Server Component 专门负责数据获取
async function DataProvider({ userId }) {
const user = await fetchUser(userId);
return <UserContext.Provider value={user} {...props} />;
}
// 在 layout 中使用
async function RootLayout() {
const user = await getUser(); // 先获取数据
return <DataProvider user={user}><App /></DataProvider>;
}
模式 4:Boundary 组件
创建"边界组件"来分隔服务端和客户端区域:
// Boundary.tsx
'use client';
export function Boundary({ children, fallback }) {
return (
<ErrorBoundary fallback={fallback}>
<Suspense fallback={fallback}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// ServerComponent.tsx
async function ServerComponent() {
return (
<Boundary fallback={<Loading />}>
<HeavyClientComponent />
</Boundary>
);
}
流式 SSR 与 React 18
什么是流式 SSR
传统的 SSR 是"同步"的:服务端必须等所有数据都准备好,才能开始发送 HTML。
请求 → 服务端获取所有数据 → 服务端渲染完整 HTML → 发送 HTML → 客户端 hydration
流式 SSR(Streaming SSR)是"异步"的:服务端可以先发送 HTML,再逐步流式发送更多内容。
请求 → 服务端发送初始 HTML
→ 开始获取数据 A(同时浏览器解析已收到的 HTML)
→ 数据 A 就绪,发送对应的 HTML 片段
→ 开始获取数据 B
→ 数据 B 就绪,发送对应的 HTML 片段
→ ...
React 18 的 Suspense 与流式渲染
React 18 的 <Suspense> 组件与流式 SSR 深度集成:
// app/blog/page.tsx
async function BlogPage() {
return (
<html>
<body>
<h1>My Blog</h1>
<Suspense fallback={<PostListSkeleton />}>
<PostList /> {/* PostList 内部可以 await */}
</Suspense>
</body>
</html>
);
}
// app/components/PostList.tsx
async function PostList() {
// 这个 await 不会阻塞初始 HTML 发送
const posts = await db.posts.findMany();
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
工作流程:
- 服务端立即发送
<PostListSkeleton />作为 fallback - 同时,
PostList组件在服务端后台await数据 - 数据就绪后,React 自动用实际内容替换 skeleton
服务端错误处理
Suspense boundary 也会处理服务端错误:
<Suspense fallback={<Loading />}>
<ErrorBoundary fallback={<Error />}>
<ProblematicServerComponent />
</ErrorBoundary>
</Suspense>
- 如果
ProblematicServerComponent在服务端抛出错误:Error Boundary 显示<Error /> - 如果
ProblematicServerComponent在客户端 hydration 时失败:Error Boundary 显示<Error />
实践指南
什么时候用 Server Component
- 数据获取逻辑
- 访问服务端资源(数据库、文件系统)
- 大型依赖(heavy libraries)不需要发送到客户端
- 纯展示组件
什么时候用 Client Component
- 需要使用 React Hooks(useState、useEffect、useContext)
- 需要浏览器 API(window、document、localStorage)
- 需要事件监听(onClick、onChange)
- 需要动画(requestAnimationFrame、transition)
迁移策略
从传统 React SSR 迁移到 RSC:
1. 从叶子节点开始
// 原来:整个组件是 Client Component
'use client';
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data && <Chart data={data} />}</div>;
}
// 迁移后:只把需要交互的部分改成 Client Component
// Dashboard.tsx (Server Component)
async function Dashboard() {
const data = await fetchData();
return <ChartComponent data={data} />; // 传递数据
}
// ChartComponent.tsx (Client Component)
'use client';
function ChartComponent({ data }) {
return <Chart data={data} />;
}
2. 识别"数据获取 + 展示"的模式
// 原来:数据获取和展示混在一起
'use client';
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => { fetchUser().then(setUser); }, []);
// ...
}
// 迁移后:分离关注点
// UserProfilePage.tsx (Server Component)
async function UserProfilePage({ userId }) {
const user = await fetchUser(userId);
return <UserInfo user={user} />; // 只传递必要数据
}
// UserInfo.tsx (Client Component)
'use client';
function UserInfo({ user }) {
// 只负责交互,不再负责数据获取
}
常见问题
Q:Server Component 能不能用 useState?
不能。Server Component 没有状态,因为它们不参与 React 的更新机制。
Q:Server Component 能不能用 Context?
不能直接用。但可以通过 Server Component 作为 Provider 的子元素来间接使用。
Q:一个组件能同时是 Server 和 Client Component 吗?
不能。每个组件在运行时要么是 Server Component,要么是 Client Component。但一个组件可以:
- Server Component(在服务端渲染)
- Client Component(在客户端 hydration)
Q:Server Component 抛出的错误谁来处理?
服务端渲染时的错误会被最近的 Error Boundary(如果有)捕获。如果没有 Error Boundary,会导致整个请求失败。
延展阅读
- React 官方文档 — Server Components — RSC 官方参考
- Next.js 官方文档 — Server Components — Next.js App Router 中的 Server Components
- Next.js 官方文档 — Client Components — Client Components 使用指南
- React Team — RFC: Server Components — RSC 的原始 RFC
- Dan Abramov — Next.js 13 Is Live — 对 RSC 的深入分析
- Josh Comeau — The Perils of Rehydration — 理解服务端渲染和客户端 hydration 的差异