跳转到内容

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 解析库,浏览器收不到那 100KB JS——只收到 RSC 序列化后的节点描述(常称 Flight payload),不是 SSR 那种整页 HTML 字符串

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

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

案例 1:server component 直连数据库

Section titled “案例 1:server component 直连数据库”

分三步看:

  1. 文件没有 'use client' → 默认是 server component(厨房)
  2. 组件写成 async,直接 await 查库——浏览器端做不到
  3. 返回的 JSX 会序列化成节点描述发给客户端;db 对象永远不进 bundle
// 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>
}

分三步:① server 取数;② 只把可序列化的 postId(数字)传给 client;③ client 用 useState 做点赞交互。

// 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>
<LikeButton postId={post.id} /> {/* 边界:可序列化 props */}
</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>
}

换成 onClick={...} 当 props 从 server 传入会报错——函数过不了序列化边界。

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

Section titled “案例 3:常见误区——把 SSR 当 RSC”

SSR(服务端渲染):把客户端组件先在服务端画成 HTML 字符串发给浏览器,再 hydrate(把已有 HTML 和 JS 事件绑在一起)。这些组件的 JS 仍会进 bundle

RSC:组件只存在于服务端,浏览器拿到的是序列化后的 React 节点描述(Flight),没有「给这段 UI hydrate」这一步,因为本来就没事件处理器。

一句话:SSR 优化首屏 HTML;RSC 优化 bundle 大小 + 数据获取路径。常一起用,不是同一回事。

// 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。组合模式:交互壳在浏览器,内容仍可在厨房做。

  1. 'use client' 当性能优化:越往叶子加 bundle 越小,越往加越大。判断标准是「这块要不要交互 / 状态 / 浏览器 API」
  2. 传非序列化 props<Child onClick={fn} /> 从 server 传 client 直接报错。把 fn 放进 client 文件,或用 Server Actions(React 19)
  3. 在 server component 里用 hooksuseState / useEffect / useContext 全部失败
  4. 以为 'use client' 就完全脱离 server:错。该文件仍会先 SSR 再 hydrate;'use client' 只表示「代码也要送到浏览器」

适用

  • Next.js App Router(建议 13.4+) 为主、已接好 RSC 运行时的元框架
  • 中小项目想直连数据库、少写一层 REST/API
  • 页面里交互热点少(例如 <30% 组件需要 useState),想明显缩 JS bundle

不适用

  • 纯 SPA(CRA / 仅客户端 Vite)— 没有服务端执行环境
  • 强离线 PWA — server component 依赖在线请求
  • Next.js Pages Router — RSC 只在 App Router
  • 2020 年 12 月:React 发 RFC 0188 + demo,组件直接读 markdown,浏览器几乎只剩 React 运行时
  • 2022 年:Next.js 13 引入 App Router(当时仍偏 beta);2023 年 13.4 标稳定,RSC 才广泛用于生产
  • 2024 年:React 19 把 RSC(及配套的 Server Actions)标为稳定,从 RFC 变一等公民
  1. 'use client' 是边界声明,不是优化标记——React 团队把「默认 server、显式标 client」写成设计决策
  2. 三个边界要分开看:执行 / 打包 / 序列化,讨论时别混成一件事
  3. RSC ≠ SSR:前者是「组件只在服务端存在」,后者是「客户端组件预渲染成 HTML」
  4. 组件第一次有了「在哪运行」:以前默认都是浏览器里的纯 UI 函数
  • react-hooks —— 老 React 心智模型;client component 仍然遵循
  • nextjs-app-router —— RSC 第一个稳定宿主
  • suspense-boundaries —— RSC 异步渲染依赖 Suspense 表达 loading
  • server-actions —— RSC 的姊妹机制,让 client 调 server 函数
  • islands-architecture —— Islands Architecture — 静态页面里只让需要交互的小块加载 JS
  • nvm —— nvm — 在同一台机器上轻松切换 Node 版本
  • lexical —— Lexical — 把富文本编辑拆成快照、事务和插件
  • next-intl —— next-intl — Next.js 专用的多语言开关
  • nivo —— nivo — React + d3 组件化图表