跳转到内容

React Server Components — 让组件自己决定在哪台机器跑

是什么

React Server Components(RSC)是 React 团队 2020 年提的一个 RFC,把组件分成 两类

  • 服务器组件:默认。只在服务端执行,永远不会被打进浏览器 JS bundle
  • 客户端组件:文件顶部写 'use client' 才算。这部分才会送到浏览器

日常类比:像餐厅厨房和前厅。厨房组件(server)拿原料、看库存、做菜,做完只把盘子端出来;前厅组件(client)和顾客互动——按按钮、填表、点单。顾客永远进不了厨房,厨房代码也不会出现在客人桌上。

为什么重要

不理解 RSC,下面这些事都没法解释:

  • 为什么 Next.js 13 App Router 里写 async function Page() 能直接 await db.query(...),但同一个写法在 Pages Router 报错
  • 为什么 'use client' 这一行有时让 bundle 变小(边界往上推),有时反而变大
  • 为什么传给客户端组件的 props 不能是函数——序列化边界硬约束
  • 为什么 RSC 和 SSR(服务端渲染)是两件事,虽然都”在服务端跑”

核心要点

RSC 的设计可以拆成 三个边界

  1. 执行边界:server component 跑在 Node / Edge,能 await fetchimport('fs')、连数据库;client component 跑在浏览器,能 useState / useEffect / 监听点击

  2. 打包边界:server component 的代码不进 JS bundle。一个 server component 用了 100KB 的 markdown 解析库,浏览器收到的只是渲染好的 HTML 片段,0 字节 JS

  3. 序列化边界:server → client 传 props 时必须是 JSON 可序列化的值。函数、class 实例、Date 的方法都过不去;children 是特殊豁免(React 知道怎么序列化它们)

'use client' 这一行不是性能标记,是边界声明——告诉打包器:“从这个文件开始,下面的子树要打到浏览器”。

实践案例

案例 1:server component 直连数据库

// app/posts/page.tsx — 没有 'use client',默认 server
import { db } from '@/lib/db'
export default async function Posts() {
const posts = await db.post.findMany()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

注意 async function 组件 + 直接 await——客户端组件做不到。这段代码里 db 这个对象永远不会出现在浏览器

案例 2:边界划分

// app/page.tsx — server
import LikeButton from './like-button' // client component
export default async function Page() {
const post = await db.post.find(1)
return (
<article>
<h1>{post.title}</h1> {/* server 渲染 */}
<LikeButton postId={post.id} /> {/* 边界! */}
</article>
)
}
app/like-button.tsx
'use client' // ← 这一行是边界
import { useState } from 'react'
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false)
return <button onClick={() => setLiked(!liked)}>{liked ? '' : ''}</button>
}

postId 是数字,能跨边界。换成 onClick={...} 当 props 传则报错。

案例 3:常见误区——把 SSR 当 RSC

SSR 是把已经存在的客户端组件预渲染成 HTML 字符串发给浏览器,浏览器再 hydrate(绑事件)。这些组件的 JS 代码还是会被打包

RSC 是组件只存在于服务端,浏览器拿到的是序列化后的 React 节点描述(不是 HTML 字符串),没有 hydrate 步骤因为本来就没事件。

差别用一句话说:SSR 优化”首屏速度”,RSC 优化”bundle 大小 + 数据获取路径”。两件事经常一起用,但不是同一回事。

案例 4:把 client 组件当 children 传

// app/layout.tsx — server
import Sidebar from './sidebar' // client
export default function Layout({ children }) {
return <div><Sidebar>{children}</Sidebar></div>
}

children 这里如果是 server component,它会先在服务端渲染好再作为序列化节点传给 client Sidebar。这是 RSC 最强的组合模式:让 client 壳包 server 内容。

踩过的坑

  1. 'use client' 当性能优化:很多人见到 bundle 大就到处加,其实越往叶子加 bundle 越小,越往加越大。正确的判断是”这块需不需要交互/状态/浏览器 API”

  2. 传非序列化 props<Child onClick={fn} /> 从 server 传 client 直接报错。解决方式要么把 fn 内联到 client 组件里、要么用 server action(React 19 的扩展机制)

  3. 在 server component 里用 hooksuseState / useEffect / useContext 全部失败。新手常见错误是从 client 复制过来忘了改

  4. 以为加了 'use client' 就完全脱离 server:错。这种文件还是会先在服务端预渲染一遍(SSR 那种),然后再 hydrate。'use client' 只是说”这部分代码也要送到浏览器”

适用 vs 不适用场景

适用

  • Next.js App Router 应用(13+)/ Remix / 后续支持 RSC 的元框架
  • 需要直连数据库但不想自己写 API 层的中小项目
  • 想缩 bundle 但保留 React 心智模型的团队

不适用

  • 纯 SPA(Create React App / 旧 Vite)— 没有服务端执行环境
  • 强离线 PWA — server component 必须在线
  • Next.js Pages Router — RSC 只在 App Router 工作

学到什么

  1. 'use client' 是边界声明,不是优化标记——这是 ADR-5 的核心
  2. 三个边界要分开看:执行 / 打包 / 序列化。三件事经常被混在一起讨论
  3. RSC ≠ SSR:前者是”组件只在服务端存在”,后者是”客户端组件预渲染”。同一个项目可以两者都用
  4. 从 React 视角看:组件第一次有了”在哪运行”的属性。之前的组件都是位置无关的纯 UI 函数

历史小故事(可跳过)

  • 2020 年 12 月:React 团队发 RFC + demo 视频,演示一个组件直接读 markdown 文件并渲染,bundle 只有 React 本身
  • 2022 年:Next.js 13 推 App Router,第一次稳定落地 RSC
  • 2024 年:React 19 把 RSC + Server Actions 正式 GA,从 RFC 变成 React 的一等公民

延伸阅读

关联

  • react-hooks —— 老 React 心智模型;client component 仍然遵循
  • nextjs-app-router —— RSC 第一个稳定宿主
  • suspense-boundaries —— RSC 异步渲染依赖 Suspense 表达 loading
  • server-actions —— RSC 的姊妹机制,让 client 调 server 函数

反向链接

  • expo —— Expo — RN 的”开箱即用”工具链 + 云构建 + OTA 更新
  • flutter —— Flutter — Google 自绘像素的跨平台 UI 框架
  • hindley-milner —— Hindley-Milner — 编译器自己猜变量类型
  • islands-architecture —— Islands Architecture — 静态页面里只让需要交互的小块加载 JS
  • next-intl —— next-intl — Next.js 专用的多语言开关
  • nivo —— nivo — React + d3 组件化图表
  • react-native —— React Native — 用 React 写、编译成真正的原生 App