RSC(React Server Components)详解

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 里面包含三样东西:

  1. 服务端组件渲染出来的文本、DOM 结构;

  2. 占位符:标记「这里有一个客户端组件」;

  3. 传给客户端组件的 props(必须是可序列化的值,不能传函数、Promise)。

Next 同时会输出 HTML,用来做首屏快速展示;浏览器拿到 HTML + RSC Payload + 客户端 JS 之后:

  1. 先展示静态 HTML(白屏时间短);

  2. React 在前端用 Payload 重建组件树,把服务端部分和客户端部分拼在一起;

  3. 对客户端组件做水合,绑定事件,让页面可交互。

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 后端项目,数据流是这样的:

  1. Layout / Page 是 RSC(服务端组件):渲染导航、页面骨架;不直接拉车辆列表这种高频可变数据;

  2. 页面里面引入 CarList('use client');

  3. 浏览器拿到页面后,CarList 里的 useQuery 发起请求 /api/cars;

  4. 请求打到 Next,经过 rewrite 代理转发到 Java 后端;

  5. Java 返回 JSON,TanStack Query 在前端管理 loading、缓存、重试。

这里有一个工程选型思考:为什么不直接在 RSC 里 await 调 Java?

  • **好处:**首屏直接拿到数据,不需要前端再请求;

  • **坏处:**翻页、筛选、实时刷新很难做,每次筛选都要整段 RSC 重新渲染;

  • **主流方案:**骨架 RSC,交互数据交给客户端 TanStack Query。

限制与踩坑清单

  1. RSC 里不能传非序列化 props 给客户端组件:不能传 function、Promise、Symbol;

  2. RSC 里没有 React Context,如果要全局状态,必须把 Context Provider 放到 'use client';

  3. 第三方组件库大多依赖 useState / context,导入后会强制变成客户端边界;

  4. RSC 的报错和客户端报错链路不一样,Turbopack HMR 会区分:改 RSC 只更新 Payload,不会重载客户端 JS;

  5. RSC 里的 fetch Next 自带缓存、revalidate;如果直接用 axios,不会自带缓存能力;

  6. 中间件(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 最本质的区别。

相关推荐
To_OC2 天前
同样是让 AI 写全栈,为什么别人一句话就够了
前端·前端框架·next.js
FungLeo6 天前
成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页
react.js·响应式设计·next.js·移动端适配·成为全栈·web 可访问性
FungLeo7 天前
成为全栈·Next.js 网站前台篇·站点设置如何驱动页头、页脚与 SEO
react·isr·next.js·metadata·成为全栈·站点配置
FungLeo9 天前
成为全栈·Next.js 网站前台篇·同源 BFF 代理:API、附件、Cookie 与跨站写入保护
react·csrf·next.js·bff·成为全栈·route handler
FungLeo9 天前
成为全栈·Next.js 网站前台篇·评论系统:叠楼、回复、删除与内容审核如何落到前台
react.js·内容审核·next.js·评论系统·树形数据·成为全栈
cindershade12 天前
一条请求的接力赛:用运行时责任重画 RSC 边界
性能优化·next.js
zach13 天前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
zach13 天前
Vue/React SPA 打包部署后,子路由刷新 401 未登录问题彻底解决
前端·nginx·next.js
FungLeo15 天前
成为全栈·Next.js 网站前台篇·数据获取与缓存:60 秒再验证背后到底发生了什么
react·isr·next.js·成为全栈·react cache·opennext