React Server Components(RSC)是 React 团队推出的一种新组件类型,它重新定义了组件在何处运行以及如何与客户端协作。与传统的客户端渲染或者服务端渲染(SSR)不同,RSC 允许部分组件只在服务器上执行,而另一部分组件在浏览器中运行,两者可以无缝组合在同一棵组件树中。这种混合模型并不是要取代现有的客户端组件,而是提供了一种更细粒度的能力划分方式,让开发者能够根据组件的职责选择运行环境。

理解 RSC 的关键在于区分组件代码的“执行位置”和“渲染输出”。服务端组件在服务器上运行,可以直接访问后端资源,比如通过 ORM 查询数据库、读取文件系统或者调用内部 API。它们不会被包含在发送给浏览器的 JavaScript bundle 中,因此可以大幅减少前端需要下载的代码量。客户端组件则和传统的 React 组件一样在浏览器中执行,负责处理用户交互、管理本地状态以及使用浏览器专属 API。通过一种称为“序列化树”的传输格式,服务端组件渲染出的结果会被流式发送到客户端,其中穿插着客户端组件的占位符。
服务端组件与客户端组件的边界划分
在 React Server Components 模型中,组件默认是服务端组件。如果你需要让某个组件在客户端运行,就必须在该文件顶部添加 'use client' 指令。这个指令告诉打包工具该模块及其依赖的模块都需要被打包到客户端 bundle 中。需要注意的是,'use client' 只是一个边界标记,它并不改变组件本身的语法,你仍然可以像编写普通 React 组件一样使用 hooks 和事件处理器。
服务端组件和客户端组件之间有一条严格的序列化边界。服务端组件可以将数据作为 props 传递给客户端组件,但这些 props 必须是可序列化的,比如字符串、数字、布尔值、数组、普通对象以及 React 元素。函数、类实例、Symbol、Promise 等不可序列化的内容不允许直接通过 props 传递。这意味着你不能把一个事件处理函数从服务端组件传给客户端组件,也不能把从数据库查询出的 ORM 实体对象直接传过去,必须先将它们转换为普通 JavaScript 对象。
这种限制看似不便,实际上推动了更清爽的组件设计。数据获取逻辑被集中在服务端组件中,客户端组件只接收已经整理好的数据并专注于交互。举例来说,一个列表页面可以由服务端组件负责从数据库读取文章列表,然后把每篇文章的标题和摘要作为 props 传给一个客户端组件,该客户端组件内部使用 useState 管理展开和收起的状态。这样数据库查询逻辑完全不会出现在客户端代码中,也避免了在浏览器中暴露数据库连接信息。
混合开发模式的核心运行机制
React Server Components 的渲染流程从服务器上的根组件开始。框架(如 Next.js)会执行服务端组件的渲染函数,生成一个称为 RSC Payload 的序列化数据结构。这个 payload 中包含了服务端组件渲染出的文本节点、样式信息以及客户端组件的引用位置。浏览器接收到 payload 后,React 会重建组件树,对于客户端组件的引用,会加载对应的 JavaScript 模块并在客户端执行其渲染逻辑。整个过程中,服务端组件的代码不会出现在浏览器中,只有它们渲染出的结果被发送过去。
这种机制带来的一个显著优势是减少冗余代码。假设一个组件只做静态展示,不依赖任何客户端状态,那么它完全可以作为服务端组件存在,其依赖的库(比如日期格式化工具或 Markdown 解析器)也不会进入客户端 bundle。而如果同一个组件被标记为客户端组件,那么这些依赖都必须被浏览器下载。通过混合使用,开发者可以精确控制哪些代码被送到客户端,从而优化加载性能。
另一个关键点是数据获取的方式。在传统 React 应用中,数据通常在客户端通过 useEffect 发起网络请求获得,这会导致额外的往返延迟和闪屏。而在 RSC 中,服务端组件可以直接使用 async/await 获取数据,渲染完成后再把最终 HTML 片段和序列化数据返回给客户端。这样既减少了客户端的请求次数,也避免了多层数据获取造成的瀑布流问题。不过要注意,服务端组件不能使用 useState、useEffect 等客户端 hooks,因为它们只在服务器上运行一次,不存在交互状态。
实践:构建一个混合组件树
下面通过一个具体例子展示如何组合服务端组件和客户端组件。假设我们有一个用户信息卡片,希望由服务端组件从数据库读取用户数据,而卡片上的点赞按钮需要在客户端处理点击事件。首先编写服务端组件,它负责获取数据并渲染客户端组件。
// UserProfile.js(服务端组件)
import { db } from './database';
import LikeButton from './LikeButton';
export default async function UserProfile({ userId }) {
const user = await db.user.findUnique({ where: { id: userId } });
const plainUser = {
name: user.name,
avatarUrl: user.avatarUrl,
bio: user.bio,
};
return (
<section>
<h2>{plainUser.name}</h2>
<img src={plainUser.avatarUrl} alt={plainUser.name} />
<p>{plainUser.bio}</p>
<LikeButton initialCount={user.likes} />
</section>
);
}
在上面的代码中,UserProfile 是一个服务端组件,它使用 async/await 查询数据库并返回 JSX。注意 <LikeButton /> 是一个客户端组件,它接收 initialCount 作为初始点赞数。由于 user.likes 是一个数字,它满足可序列化条件,因此可以安全传递。接下来看看客户端组件的实现。
// LikeButton.js(客户端组件)
'use client';
import { useState } from 'react';
export default function LikeButton({ initialCount }) {
const [count, setCount] = useState(initialCount);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
点赞 {count}
</button>
);
}
这里 'use client' 指令告诉 React 和打包器这个文件需要被当作客户端组件处理。组件内部使用 useState 管理点赞数量,这是完全合法的,因为它会在浏览器中重新挂载和更新。用户点击按钮时,只有 LikeButton 组件重新渲染,服务端组件返回的静态内容不会受到影响。这种模式清晰地将数据获取和交互逻辑分离开来。
在实际项目中,可能需要在一个客户端组件内部再使用服务端组件吗?答案是否定的,因为一旦模块被标记为 'use client',其内部导入的所有组件都会被当作客户端组件处理,无法再恢复为服务端组件。因此,最合理的做法是将客户端组件放在组件树的叶子节点或者靠近叶子的位置,让服务端组件负责数据流的主干。如果你发现需要从客户端组件向服务端组件传递数据,正确的做法是将服务端组件作为 children 通过 props 传入,框架会自动处理序列化边界。
常见限制与最佳实践
使用 React Server Components 时最常遇到的限制就是 props 序列化。除了函数和类实例之外,像 Date 对象、Map、Set 等也需要先转换为普通对象或数组。另一个容易遗漏的点是环境变量的使用:客户端组件中无法访问服务器端的环境变量,必须通过显式的 props 传递。如果你的客户端组件依赖了某个只在 Node.js 中可用的包,就需要考虑将其拆分为服务端组件,或者使用动态导入在客户端按需加载。
性能方面,RSC 并不是万能的。对于高度交互的密集型应用,比如实时协作编辑器或者复杂的表单,过度使用服务端组件可能会导致大量序列化数据传输和反复重建。此时应该把交互密集的部分放在客户端组件中,服务端组件只负责外壳和初始数据。此外,服务端组件不应该包含任何定时器、订阅或浏览器事件监听,因为这些逻辑在服务器上毫无意义。遵循“服务端组件获取数据,客户端组件处理交互”的原则有助于保持代码清晰。
在实际开发中,可以使用 Next.js 的 App Router 来体验 RSC。创建一个 page.js 作为服务端组件,然后在其中导入带有 'use client' 指令的组件。Next.js 会自动处理 RSC 的构建和序列化,你只需关注组件本身的职责划分。随着 React 19 的稳定,Server Components 已经成为正式特性,理解其混合开发模式对于构建现代 React 应用至关重要。建议从小型模块开始实践,逐步将数据获取逻辑迁移到服务端组件,并观察 bundle 体积和首屏加载时间的变化。
React Server Components服务端组件客户端组件修改时间:2026-08-20 16:51:07