服务端组件 vs 客户端组件:React 新范式下的组件划分原则

React 服务端组件(RSC)改变了传统 SSR 的组件模型,很多团队在组件划分上犯了难。本文从渲染机制、数据访问、交互边界出发,分析服务端组件与客户端组件的核心区别,总结组件划分原则、常见误区和落地步骤,帮助你在 RSC 架构下合理设计组件边界,避免 bundle 膨胀与水合性能问题。

RSC 不是 SSR,但很多团队搞混了

当 React 团队正式把服务端组件(RSC)推向生产环境,并借着 Next.js App Router 进入主流视野后,很多前端团队在组件设计上多了一个纠结:这个组件到底该写在服务端还是客户端?

AI technology illustration

先说结论:服务端组件和客户端组件不是一种二选一的标签,而是一条组件树上的职责边界。理解这条边界在哪里、为什么在那里,才是划分组件的正确姿势。

在过去,我们熟悉的 React 应用是纯客户端渲染。即使使用 Next.js 的 SSR,也只是把首屏的 HTML 在服务端渲染出来,然后所有 JavaScript 都会打包发送到浏览器,在客户端进行水合(hydration)。这套模型里,所有组件本质上都是客户端组件。而 RSC 改变了这个前提:组件可以只运行在服务端,它的代码不会进入客户端 bundle,它的数据读取直接在服务端完成,不需要经过 API 往返。

为什么这个模型重要?因为传统 SSR 只解决了首屏渲染速度,没有解决 bundle 体积和数据请求效率。服务端组件把“获取数据”和“渲染静态内容”这两件事留在服务端,客户端只承担真正需要交互的部分。这也是 RSC 能带来实质性能提升的根本原因。

但这里有一个常见的误区:很多人把服务端组件和 SSR 混为一谈。SSR 是一种渲染策略,RSC 是一种组件范式。Next.js App Router 默认启用 RSC,但你在页面里写的组件默认是服务端组件,只有加了 'use client' 的才是客户端组件。这种默认值让很多老 React 开发者不太适应。

服务端组件和客户端组件,到底谁管什么

要划分组件,先记住两个基本事实:服务端组件可以直接访问数据库、文件系统等 Node.js 资源,可以用 async/await 获取数据;但它不能用 useState、useEffect,不能绑定 onClick 事件。客户端组件可以使用所有 React 能力,但它的代码会被打包进浏览器,并且通常需要经过水合才能获得交互能力。

所以最核心的划分原则是:能用服务端组件就用服务端组件,除非你需要浏览器 API 或交互状态。

来看一个最典型的例子。假设你要渲染一组笔记,并支持点赞:

// app/page.jsx —— 服务端组件
export default async function Page() {
  const notes = await db.query('SELECT * FROM notes');

  return (
    <main>
      <h1>团队笔记</h1>
      <NoteList notes={notes} />
      <LikeButton noteId={notes[0].id} />
    </main>
  );
}
// components/LikeButton.jsx —— 客户端组件
'use client';

export function LikeButton({ noteId }) {
  return (
    <button => like(noteId)}>
      点赞
    </button>
  );
}

注意,NoteList 是纯展示组件,可以在服务端渲染;LikeButton 因为需要 onClick,所以必须是客户端组件。这个例子看起来简单,但实际项目中会遇到比这复杂得多的边界。

这里最关键的一点是:客户端组件可以嵌套在服务端组件里,作为“客户端边界”存在。服务端组件负责获取数据并生成静态内容,客户端组件负责接收数据并附加交互。

划分组件的三条具体原则

基于这个模型,我总结出三条可以直接用的判断原则:

  • 原则一:先写服务端组件,遇到不能解决的问题再加 ‘use client’。 如果组件里不需要状态、事件或浏览器 API,就让它留在服务端。默认服务端可以避免很多无意识的客户端代码膨胀。
  • 原则二:把客户端组件放在叶子位置。 让客户端组件尽量少而薄,避免把整棵子树都拖进客户端。一个常见反模式是在页面根节点加 ‘use client’,导致下面所有组件都变成客户端组件。
  • 原则三:通过 props 传递数据,而不是在客户端组件里重新请求。 服务端组件拿到数据后直接传给客户端组件,减少客户端请求和状态同步,也避免出现“先 loading 后数据”的不必要闪烁。

工程里最容易踩的几个坑

原则是简单的,但在真实项目里,你会遇到几个反复出现的坑。

第一个坑:把服务端组件当成只能做静态展示的组件。 有的团队会认为服务端组件里不能写任何逻辑,只能返回 JSX。其实服务端组件可以执行任何 async 函数,你可以直接在这里查数据库、调内部服务,甚至做权限校验。只要最终渲染的产物是可序列化的内容,都没问题。

第二个坑:在客户端组件里直接导入服务端组件。 React 不允许这样做,因为服务端组件不能打包进客户端。但你可以通过 children 把服务端组件作为 props 传入客户端组件,形成“插槽”模式。这个模式在需要把某个客户端状态包裹到服务端内容上时非常有用:

// client-wrapper.jsx —— 客户端组件
'use client';
export function ClientWrapper({ children }) {
  const [open, setOpen] = useState(false);
  return <div>{open && children}</div>;
}

// page.jsx —— 服务端组件
export default async function Page() {
  const data = await fetchData();
  return (
    <ClientWrapper>
      <ServerContent data={data} />
    </ClientWrapper>
  );
}

第三个坑:忽略序列化限制。 服务端组件给客户端组件传 props 时,只能传可序列化的值。函数、Date、Map 这些对象不能直接传。如果你确实需要把事件处理函数传给客户端组件,通常要借助 Server Actions 或另外定义回调接口,而不是直接塞一个 function。

第四个坑:React Context 的边界问题。 Context 是客户端机制,在服务端组件中不生效。如果你在根布局里放了 Provider,那么所有消费这个 context 的组件都必须是客户端组件。很多团队把主题、用户信息等全局状态放在 Provider 里,结果整个页面树都被拖进了客户端,RSC 的收益就所剩无几了。合理的做法是把 Provider 限制在真正需要它的客户端子树里,而不是放在根布局。

不同方案的对比与适用场景

为了让你更直观地判断,我把服务端组件和客户端组件的关键差异整理成一张表:

维度 服务端组件 客户端组件
渲染位置 服务端 客户端(水合后)
数据访问 可直接访问数据库/内部服务 通过 API/请求
交互能力 不支持事件、hooks 支持全部 React 能力
Bundle 体积 不进入客户端 bundle 代码会打包发送
典型用途 数据读取、静态内容、页面骨架 表单、弹窗、实时更新

这并不意味着客户端组件是“二等公民”。在交互密集的应用里,客户端组件依然是主角。RSC 的价值在数据密集、交互稀疏的场景下最明显——比如内容平台、电商列表页、数据报表。如果你的页面几乎全部是交互组件,那强行把一部分改成服务端组件收益有限,反而增加心智负担。

从老项目迁移时的落地建议

如果你正在把现有 React 项目迁移到 Next.js App Router,或者刚开始在一个新项目里启用 RSC,可以按下面的顺序来:

  1. 先盘点依赖。 找出所有使用了 useState、useEffect、useContext、事件处理的组件,这些必须标记为客户端组件。
  2. 抽离纯展示组件。 把不依赖浏览器状态的展示组件改成服务端组件,让父级在服务端直接传入数据。
  3. 收敛 Provider。 把全局 Provider 收拢到一个客户端根组件中,尽量不阻塞服务端组件对数据的直接读取。
  4. 统一代码约定。 在文件命名上区分,比如用 .client.jsx 后缀标记客户端组件,让团队在代码审查时一眼看出边界。

需要提醒的是,不要为了优化而优化。如果你的页面本来就不大,水合成本低,强行拆分反而会增加维护成本。服务端组件最适合的是那些“数据多、交互少”的页面。团队在推进时可以先用性能分析工具定位真正的瓶颈,再决定是否要扩大服务端组件的范围。

最后:把边界当作设计的一部分

服务端组件和客户端组件并不是竞争关系,它们共同构成一个完整的组件树。你可以把服务端组件想象成数据管道和静态骨架,把客户端组件想象成交互皮肤。真正重要的是让你的团队理解这条边界,并在实践中不断调整。

当有人再问“这个组件该放服务端还是客户端”,你可以回答:先看它需不需要浏览器,再看它能不能用服务端的数据,最后决定哪里放。这个思路比背标签实用得多。

原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/488/

(0)
上一篇 56分钟前
下一篇 25分钟前

相关推荐