RSC 是 React 官方的组件模型,把组件拆成「服务端组件」和「客户端组件」。Next.js App Router 默认页面、Layout 都是 RSC(服务端组件),代码只在服务器执行,不会打包下发浏览器;只有写了
'use client'的组件才跑到浏览器,支持 useState、点击交互和 TanStack Query。
先纠正一个高频误区:RSC ≠ SSR。
SSR:组件代码在服务端跑一遍出 HTML,同一份 JS 还要发给浏览器再跑一遍做水合;
RSC:服务端组件代码永远留在服务器,根本不打包到前端 JS 包,浏览器只收到它渲染后的结果。
为什么需要 RSC
老 React(SPA / Pages Router 的 SSR)有个天生痛点:不管这个组件要不要交互,只要写了 React,JS 代码都会打包到前端。哪怕只是纯展示的标题、列表、文章,也要下载 JS、解析、执行、水合。页面越大,JS 包越大,首屏越慢。而且数据请求经常在客户端发起,要等 JS 下载完才能请求接口,有额外的网络瀑布。
React 团队设计 RSC 的目标:
-
减少发给浏览器的 JS 体积:纯展示、数据拉取类组件留在服务端,不打包;
-
数据请求放到服务端:直接连数据库、内网后端,不用走前端跨域;
-
自然拆分「展示层」和「交互层」;
-
支持流式渲染、细粒度缓存、部分刷新。
对应到典型项目里:页面骨架、分类标题、静态描述放在 RSC;搜索框、下拉筛选、车辆列表(TanStack Query 请求、loading、分页)放在 'use client' 客户端组件。
服务端组件 vs 客户端组件
服务端组件(默认就是它,不需要任何标记)
App Router 里面,page.tsx / layout.tsx 默认就是服务端组件。
**运行环境:**Node / Edge Runtime(服务器),代码不会打包下发浏览器。
可以做:
-
直接
await拉数据(Prisma、直接调后端接口); -
读取服务端环境变量(不带
NEXT_PUBLIC_的); -
文件系统操作、数据库查询;
-
async 组件(
async function Page()直接写)。
不能用:
-
useState/useEffect/useRef等 React 状态 Hook; -
window/document浏览器 API; -
点击事件、表单交互;
-
TanStack Query(它依赖客户端状态)。
tsx
// 默认 RSC,不需要任何标记
async function Page() {
// 服务端直接拿数据,可以直接 prisma,或者直接请求后端接口
const data = await fetch("http://localhost:8082/home");
const list = await data.json();
return <h1>首页标题 {list.title}</h1>;
}
客户端组件(Client Component)
文件顶部写 'use client',作为边界标记:这个文件以及它所有子依赖,全部打包到浏览器,在前端执行。
**运行环境:**浏览器,JS 会打包下发,需要水合。
可以用: useState、点击事件、useEffect、TanStack Query、监听 window。
**不能用:**直接访问 Prisma、读取服务端私有环境变量。
tsx
'use client'; // 边界!
import { useQuery } from '@tanstack/react-query';
export function CarList() {
const { data } = useQuery({ queryKey: ['cars'], queryFn: fetchCarApi });
return <div>{data?.map(...)}}</div>;
}
**边界规则:**RSC(服务端组件)可以导入客户端组件;客户端组件不能反过来导入服务端组件,只能通过 props 传序列化数据进去。
RSC Payload:浏览器到底收到了什么
很多人卡在这里:服务端组件渲染完,发给浏览器的不是普通 HTML,而是 RSC Payload,一种序列化的结构化数据(不是 JSX)。
Payload 里面包含三样东西:
-
服务端组件渲染出来的文本、DOM 结构;
-
占位符:标记「这里有一个客户端组件」;
-
传给客户端组件的 props(必须是可序列化的值,不能传函数、Promise)。
Next 同时会输出 HTML,用来做首屏快速展示;浏览器拿到 HTML + RSC Payload + 客户端 JS 之后:
-
先展示静态 HTML(白屏时间短);
-
React 在前端用 Payload 重建组件树,把服务端部分和客户端部分拼在一起;
-
对客户端组件做水合,绑定事件,让页面可交互。
RSC 和 Turbopack 的关系
这就是为什么 Turbopack 需要「统一依赖图 Unified Graph」。一个项目里同时存在两张模块图:
-
服务端模块图:RSC、layout、page,可以调用 Prisma;
-
客户端模块图:
'use client'之下的组件,打包给浏览器。
Webpack 时代 Next 需要两套独立编译器分别打包两张图,很容易出现解析不一致;Turbopack 一张图,自动识别 'use client' 边界,自动切分两套产物,这也是 Turbopack 专门为 RSC 设计的核心价值。
RSC、SSR、SSG 快速区分
| 概念 | 执行时机 | 代码是否下发浏览器 |
|---|---|---|
| SSG | build 阶段提前渲染成静态 HTML,CDN 缓存 | 同 SSR,组件 JS 会下发 |
| SSR | 每次请求服务端渲染 HTML | 同一份 JS 两边跑,组件代码下发前端做水合 |
| RSC | 服务端渲染,可静态可动态可流式 | 服务端组件代码永远不下发;只有 'use client' 部分下发 |
容易混淆的指令:
'use client':标记客户端组件(组件在浏览器跑);
'use server':标记 Server Action(服务端函数,前端调用,在后端执行);
没有 'use server' 来标记 RSC,RSC 是默认的。
放到业务里的数据流
典型的 Next16 + App Router + RSC + TanStack Query + Java 后端项目,数据流是这样的:
-
Layout / Page 是 RSC(服务端组件):渲染导航、页面骨架;不直接拉车辆列表这种高频可变数据;
-
页面里面引入
CarList('use client'); -
浏览器拿到页面后,CarList 里的 useQuery 发起请求
/api/cars; -
请求打到 Next,经过 rewrite 代理转发到 Java 后端;
-
Java 返回 JSON,TanStack Query 在前端管理 loading、缓存、重试。
这里有一个工程选型思考:为什么不直接在 RSC 里 await 调 Java?
-
**好处:**首屏直接拿到数据,不需要前端再请求;
-
**坏处:**翻页、筛选、实时刷新很难做,每次筛选都要整段 RSC 重新渲染;
-
**主流方案:**骨架 RSC,交互数据交给客户端 TanStack Query。
限制与踩坑清单
-
RSC 里不能传非序列化 props 给客户端组件:不能传 function、Promise、Symbol;
-
RSC 里没有 React Context,如果要全局状态,必须把 Context Provider 放到
'use client'; -
第三方组件库大多依赖 useState / context,导入后会强制变成客户端边界;
-
RSC 的报错和客户端报错链路不一样,Turbopack HMR 会区分:改 RSC 只更新 Payload,不会重载客户端 JS;
-
RSC 里的
fetchNext 自带缓存、revalidate;如果直接用 axios,不会自带缓存能力; -
中间件(middleware.js)属于 Edge Runtime,不属于 RSC,它跑在请求最前面,比 RSC 渲染更早。
一句话总结
RSC 是 React Server Components,App Router 默认组件就是服务端组件,代码在服务器执行,不会打包下发浏览器;
'use client'作为边界标记客户端组件,代码下发浏览器、支持状态和交互;渲染后传输 RSC Payload,服务端只输出结构化数据和 HTML,以此减少前端 JS 体积、优化首屏。Turbopack 通过统一依赖图自动区分两张模块图,支持 RSC 的编译。
小结
RSC 不是简单的「又一种 SSR」,它是 React 组件运行模型的重构。它把「组件在哪跑」这件事显性化,让开发者主动划分:哪些内容是纯展示、放服务器;哪些需要交互、放浏览器。这也是 Next App Router 和老 Pages Router 最本质的区别。