很多前端团队在从传统 CSR 或 SSR 方案迁移到 Next.js App Router 时,都会遇到一个共同的困惑:React Server Components(RSC)到底解决了什么问题?它和 SSR 有什么区别?为什么应该把组件拆成服务端和客户端两部分?这篇文章不会逐条翻译官方文档,而是从真实工程场景出发,梳理 RSC 的运作逻辑、边界判断、性能要点以及生产环境中最容易踩的坑。

RSC 不是另一种 SSR
理解 RSC 的第一步,是把它和 SSR 彻底分开。SSR 解决的是首屏白屏和 SEO 问题,服务器生成 HTML 字符串,客户端接收后 hydration,整个页面仍然需要下载完整的组件 JavaScript 并执行。RSC 则完全不同:服务端组件只在服务器上运行,产生的输出是一种特殊的序列化格式,不包含任何组件代码,客户端永远不会拿到这些组件的 JS 源码。简单说,RSC 是零客户端 JS 的组件。
这个区别直接影响了数据获取方式。在传统的 SSR 中,数据往往通过 getServerSideProps 这种顶层函数获取,然后向下传递。而 RSC 可以让组件自己 await 数据,天然支持异步渲染,不再需要层层传递 props 或使用全局状态管理来灌数据。
运行机制:序列化、流与组件树
RSC 的输出并不是 HTML,而是一种 React 内部定义的序列化格式,其中包含了原生 HTML 片段、客户端组件的占位符以及这些客户端组件需要的 props(必须是可序列化的)。当服务器渲染一棵包含 RSC 和客户端组件的树时,React 会流式地将这些序列化数据发送给客户端,客户端按序解析,遇到占位符时渲染对应的客户端组件。
下面是一个非常典型的服务器组件示例,它直接查询数据库并返回渲染结果:
// app/products/page.tsx
export default async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
这里的 db.query 直接在组件内部执行,没有任何客户端 JavaScript 参与,产品列表的 HTML 会被序列化并流式传输给浏览器。而如果需要一个“加入购物车”按钮,我们就必须使用客户端组件,因为服务器组件里不能有交互逻辑:
// app/components/AddToCartButton.tsx
'use client';
export default function AddToCartButton({ productId }) {
const handleAdd = () => { /* ... */ };
return <button
}
关键点在于,服务器组件和客户端组件可以交错嵌套,服务器组件可以引入客户端组件,并传递可序列化的 props,但反过来不行。这一限制决定了组件拆分的顶层设计。
常见误区与边界判断
- 误区一:认为 RSC 是 SSR 的替代品
RSC 主要解决组件级的数据获取和客户端 JS 体积问题,SSR 解决的是初始渲染速度和 SEO,两者可以配合使用,但 RSC 即使在纯客户端导航中也能发挥作用(例如通过router.refresh重新获取服务器组件)。 - 误区二:看到
use client就以为是 SPA 组件
标记为use client的组件仍然会在服务器端预渲染一次(用于 SSR),只是它的代码会被打包到客户端。所以它依然可以接收来自服务器组件的 props,并参与初始 HTML 的生成。 - 误区三:觉得所有数据获取都应该放到服务器组件
这会导致客户端组件变成纯渲染层,固然可以,但有些频繁更新的数据(如实时通知、用户输入状态)更适合在客户端用fetch或 SWR 拉取,否则每次更新都要重新请求整个服务器组件子树,得不偿失。
方案对比:不同渲染策略的适用场景
下面这张表对比了三种典型的数据获取与渲染策略,可以帮助团队根据自身业务阶段做选择:
| 策略 | 首屏加载 | SEO | 客户端 JS 体积 | 数据获取灵活性 | 适用场景 |
|---|---|---|---|---|---|
| 传统 CSR | 慢,有白屏 | 差 | 大 | 高 | 后台管理系统、强交互工具 |
| SSR(无 RSC) | 较快 | 好 | 较大 | 一般(顶层获取) | 内容型网站、需要 SEO 的应用 |
| RSC + 流式 SSR | 最快(流式) | 好 | 最小 | 高(组件级获取) | 电商、媒体、复杂数据密集型应用 |
从表中可以看出,RSC 的优势在于把数据获取的粒度降到了组件级别,同时通过流式渲染让首屏内容更快到达用户眼前。但代价是增加了架构复杂度,团队需要明确区分组件类型。
生产环境中的真实痛点
真实项目里,最让人头疼的往往不是技术原理,而是组件拆分。以一个电商商品详情页为例,页面包含商品信息、评价列表、推荐商品、全局购物车按钮等。一开始团队可能会把所有东西都放在一个大的客户端组件里,因为这样最省事,状态管理也简单。但随着页面交互增多,首屏 JS 体积直线上升,流式渲染的优势完全发挥不出来。
正确做法是:从数据依赖出发,把不需要交互的元素抽成服务器组件。商品信息、评价列表、推荐商品都是纯展示,它们完全可以在服务器端渲染并流式输出,而购物车按钮、筛选器、点赞等交互部分则封装为客户端组件。这样,浏览器在收到第一段 HTML 时就能立即显示核心内容,后续的交互逻辑按需加载。
另一个场景出现在数据缓存上。很多工程师习惯在服务器组件里直接 await fetch,但 Next.js 的扩展 fetch 默认会缓存请求结果,如果缓存策略不当,会导致数据过期或重复请求。例如,一个实时库存组件如果被缓存了 5 分钟,用户看到的库存可能不准确。这时就需要显式地配置 cache: 'no-store' 或者使用 React 的 cache() 函数结合 unstable_cache 做更精细的控制。
如何平稳落地:一个渐进式迁移思路
对于已有项目,不建议一次性全量重写。比较稳妥的路径是:
- 从页面级组件开始,将
getServerSideProps改为服务器组件直接获取数据,并将之前的页面主体内容保留为客户端组件,观察效果。 - 逐步将页面中无交互的子树(如列表、卡片、段落)抽成独立的服务器组件,插入到客户端组件内部。
- 处理数据流:原本通过 props 下传的数据,改成服务器组件直接获取,客户端组件通过 props 接收可序列化部分。
- 识别不可序列化的 props(如函数、React 元素),这些地方必须保留为客户端组件,或者通过事件回调传递。
- 最终,利用
Suspense边界为不同片段的服务器组件提供独立的加载状态,实现精细的流式渲染。
这个过程中,一个关键原则是:服务器组件可以 import 客户端组件,但客户端组件不能 import 服务器组件。如果想在客户端组件中使用服务器组件,需要通过 children 或 props 传递,这个限制决定了组件树的组织方式,前期设计一定要想清楚。
性能之外:可维护性与团队协作
RSC 带来的不仅是性能提升,还有代码组织上的变化。数据获取逻辑内聚在组件内部,减少了全局状态和 props drilling,让组件更容易复用和测试。但同时也要求团队有更清晰的“组件职责”概念,否则很容易写出大量“四不像”的混合组件,导致后续维护困难。
React Server Components 不是银弹,它更适合那些数据密集、交互复杂的大型应用。对于简单的官网或工具型页面,传统 SSR 甚至 CSR 可能已经足够。但如果你正在构建一个需要良好 SEO 和极致性能的现代 Web 应用,RSC 提供了一条值得投入的路线。关键在于理解它的边界,而不是盲目把一切搬到服务器上。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/379/