大家好!最近在个人项目里用上了 React Server Components (RSC),觉得这东西有点意思,能让应用更快、更轻。以前 React 组件全在浏览器跑,现在部分移到服务器。今天我就来聊聊 RSC,从基础说起,帮你快速上手。
什么是 React Server Components?
简单说,RSC 是 React 的一种新组件类型,只在服务器上渲染,不发到浏览器。传统 React 组件(Client Components)需要在客户端执行 JavaScript,RSC 则直接输出静态内容,比如 HTML 或 JSX 树,减少了 bundle 大小和客户端负载。
为什么需要 RSC?想象一个博客页面:文章内容从数据库拉,评论列表也从服务器取。如果全用客户端组件,浏览器得下载所有代码再 fetch 数据。RSC 让服务器直接处理这些,浏览器只管交互部分。
RSC 不是 SSR(Server-Side Rendering)的翻版。SSR 是服务器生成完整 HTML,然后浏览器 hydrate(激活)成互动组件。RSC 更灵活,能混用服务器和客户端组件,数据流更高效。
图注:服务器处理数据,客户端只渲染交互。
RSC vs Client Components:区别在哪?
- RSC(Server Components):服务器独占。能直接访问数据库、文件系统,不用 API。不能用 state、effect 或事件处理器,因为不跑在浏览器。
- Client Components:浏览器执行。支持 useState、useEffect、onClick 等互动。文件加 "use client" 指令。
比较起来,RSC 像静态生成器,Client Components 负责动态。RSC 包裹 Client Components,数据从服务器直传。
比如,一个 RSC 页面组件:
import ClientButton from './ClientButton';async function Page() {const data = await fetchDataFromDB(); // 服务器直取数据,从数据库查询数据,无需额外 API 调用,减少网络开销。return (<div><h1>{data.title}</h1> {/* 静态标题,直接渲染 */}<ClientButton /> {/* 客户端组件处理点击,RSC 传递 props 给它 */}</div>);
}
'use client';import { useState } from 'react';export default function ClientButton() {const [count, setCount] = useState(0); // 使用状态钩子,浏览器端管理计数return <button onClick={() => setCount(count + 1)}>点击 {count}</button>; // 事件处理器,只在浏览器执行
}
这个示例中,Page 是服务器组件,能异步获取数据并渲染静态部分;ClientButton 是客户端组件,处理用户交互。优势:服务器数据直传,避免客户端再 fetch。
下面用 Mermaid 绘制 RSC vs SSR 对比:
图注:服务器组件更注重组件级渲染,减少 hydrate 开销。
如何工作?渲染流程拆解
RSC 的魔力在“流式渲染”。服务器不是一次性吐出整个页面,而是分块发送。浏览器边收边渲染,减少白屏时间。
流程详解:
- 用户请求页面:浏览器发送 GET 请求到服务器。
- 服务器渲染 RSC 树:从根组件开始,递归渲染,遇到 async 时等待数据(如 DB 查询)。
- 输出 RSC Payload:一种序列化格式(非纯 HTML),包含静态内容和客户端组件边界标记。
- 浏览器解析:收到 payload 后,渲染静态 HTML,然后加载 JS hydrate 客户端组件,实现交互。
优势:组件离数据近,避免客户端 API 请求造成的昂贵的客户端-服务器瀑布。传统方式得服务器 API + 客户端 fetch,现在一步到位。流式允许部分组件先渲染,比如头部先显示,而底部数据慢点无妨。
你可以打开一个 nextjs 网站,查看 script,这一堆东西就是从服务端生成发送到客户端的 RSC payload。

示例:服务端预取数据 + 客户端排序
服务器组件负责一次性把数据拉回来,客户端组件接管后续交互。
下面用 ProductList(服务器组件) + SortableTable(客户端组件)演示:服务端把商品数据一次性吐出来,客户端在浏览器里完成点击表头排序。
// 服务器组件
import SortableTable from './SortableTable';async function ProductList() {// 只在服务端跑一次const products = await db.products.find().toArray();return (<section><h2>全部商品({products.length} 件)</h2>{/* 把数据一次性塞给客户端组件 */}<SortableTable items={products} /></section>);
}
// 客户端组件
"use client";import { useState } from 'react';type Product = { id: number; name: string; price: number; stock: number };export default function SortableTable({ items }: { items: Product[] }) {const [sortKey, setSortKey] = useState<keyof Product>('name');const [asc, setAsc] = useState(true);const sorted = [...items].sort((a, b) => {const valA = a[sortKey];const valB = b[sortKey];if (typeof valA === 'string') {return asc? valA.localeCompare(valB as string): (valB as string).localeCompare(valA);}return asc ? (valA as number) - (valB as number) : (valB as number) - (valA as number);});const handleSort = (key: keyof Product) => {if (key === sortKey) setAsc((v) => !v);else {setSortKey(key);setAsc(true);}};return (<table><thead><tr><th onClick={() => handleSort('name')}>名称 {sortKey === 'name' && (asc ? '▲' : '▼')}</th><th onClick={() => handleSort('price')}>单价 {sortKey === 'price' && (asc ? '▲' : '▼')}</th><th onClick={() => handleSort('stock')}>库存 {sortKey === 'stock' && (asc ? '▲' : '▼')}</th></tr></thead><tbody>{sorted.map((p) => (<tr key={p.id}><td>{p.name}</td><td>¥{p.price}</td><td>{p.stock}</td></tr>))}</tbody></table>);
}
打包阶段,框架会给 SortableTable 单独打一份客户端 bundle;浏览器收到 HTML 后, hydrate 时由这份 bundle 接管排序逻辑。这样既享受了服务端直出首屏的秒开速度,又保留了交互。
示例:客户端组件使用 use 等待服务器组件数据获取
核心思路:
- 服务器先渲染关键内容(商品详情),立即 flush 给浏览器,首屏不等待。
- 对非关键区块(推荐列表)只“扔”一个 Promise 下去,浏览器收到后自己决定什么时候等它。
- 客户端用
use把同一个 Promise 捡起来,复用服务器已经启动的查询,既不会重复请求,也不会阻塞首屏。
// ProductDetail.server.tsx
import db from '~/lib/db';
import { Suspense } from 'react';
import RelatedProducts from './RelatedProducts.client';export default async function ProductDetail({ sku }: { sku: string }) {// 关键数据:必须有了才能继续,服务器直出const product = await db.product.one(sku);// 非关键数据:只启动查询,不 await,把 Promise 直接丢给客户端const relatedPromise = db.product.related(sku, { limit: 8 });return (<article><h1>{product.name}</h1><p className="price">¥{product.price}</p>{/* 首屏骨架先过去,数据随后流式填充 */}<Suspense fallback={<section className="related-skeleton">加载推荐…</section>}><RelatedProducts productsPromise={relatedPromise} /></Suspense></article>);
}
// RelatedProducts.client.tsx
"use client";import { use } from "react";interface Product {id: string;name: string;price: number;
}export default function RelatedProducts({ productsPromise }: {productsPromise: Promise<Product[]>;
}) {// 复用服务器创建的同一个 Promise,真正渲染前暂停const list = use(productsPromise);return (<section className="related"><h2>看了又看</h2><ul>{list.map((p) => (<li key={p.id}><span>{p.name}</span><strong>¥{p.price}</strong></li>))}</ul></section>);
}
React 首先渲染 “关键路径 “上的内容,即带价格的完整 HTML,而不会等待 Suspense 中的异步组件完成数据获取,这是第一个数据块。 然后,服务器将把这个数据块发送到客户端,并在等待 Suspense 组件的过程中保持连接打开,即等待 Promise resolve。 在 RelatedProducts 数据完成后,其 Suspense 边界被解决,另一个数据块就准备好了,并发送到客户端。 消息也是如此。总结一下:
- 浏览器快速收到带价格的完整 HTML,立即可见。
- 推荐区域先占位“骨架”,同一套 TCP 连接里后续流式补全,无需额外往返。
- 客户端组件通过 use 暂停渲染,复用服务端 Promise。
优势与注意事项
- 性能:JavaScript捆绑包中不会包含服务端组件,从而减少了JavaScript的大小。少发 JS,数据预取,流式加载。LCP、可交互耗时减少。不必反复调用所有这些服务器组件函数并将其返回值转换为树,从而减少编译和执行JavaScript所需的时间。
- 安全:敏感代码(如 DB 密钥)留在服务器,不暴露给浏览器。
- SEO友好:静态内容易被搜索引擎爬取。
但注意:RSC 不可变,不能有 state(否则用客户端组件)。导入客户端组件时,必须加 "use client"。调试时,检查服务器日志和浏览器 console。遇到异步错误,用 try-catch 包裹。
const BlogPost = async ({ id }) => {try {const post = await db.posts.get(id);return <div>{post.title}</div>;} catch (error) {console.error("Error fetching post:", error);return <div>Something went wrong. Please try again later.</div>;}
};
结语
RSC 让 React 更贴近全栈,服务器和客户端无缝协作。个人认为 RSC 提供了更高的性能上限,但是对开发者的要求更高了,它也不适合大多数项目。对于产品来说,性能常常不是第一优先级的。国内还是 SPA 应用主流,哈哈,有空可以玩玩 Next.js。