深入理解 React Server Components
基础使用、RSC 协议、流式传输格式、缓存机制与 Next.js App Router 实战
什么是 React Server Components?
定义:React Server Components(RSC)是 React 18 引入、React 19 正式稳定的服务端渲染范式。Server Components 在服务器上执行,代码永远不会发送到客户端,可以直接访问数据库、文件系统和内部 API。与传统 SSR 不同,RSC 不需要水合(Hydration),因为它们不包含交互逻辑。
涉及场景:
- 数据获取:直接在组件中
await数据库查询,无需 API 层 - 减小 bundle:服务端组件的代码(包括其依赖)不发送到客户端
- 安全访问:直接使用环境变量、数据库连接等敏感资源
- SEO 友好:服务端渲染完整 HTML,搜索引擎可索引
- 流式渲染:配合 Suspense 逐步发送 HTML,加速首屏
作用:
- 零客户端 JS:Server Component 的代码完全在服务端执行
- 直接数据访问:消除 API 层,减少请求往返
- 自动代码分割:Client Component 的
import自动变成代码分割点 - 面试新热点:RSC 的工作原理、与 SSR 的区别、组合规则是 2026 年高频考点
一、基础使用(Next.js App Router)
Server Component(默认)
jsx
// app/posts/page.tsx — 默认就是 Server Component
import { db } from '@/lib/db';
export default async function PostsPage() {
// 直接查询数据库(不暴露到客户端)
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 20,
});
return (
<main>
<h1>文章列表</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
<time>{post.createdAt.toLocaleDateString('zh-CN')}</time>
</li>
))}
</ul>
</main>
);
}Client Component
jsx
// components/LikeButton.tsx
'use client'; // 标记为 Client Component
import { useState, useTransition } from 'react';
import { likePost } from '@/actions/posts';
export function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
startTransition(async () => {
const newLikes = await likePost(postId);
setLikes(newLikes);
});
};
return (
<button onClick={handleLike} disabled={isPending}>
❤️ {likes} {isPending && '...'}
</button>
);
}Server Actions
jsx
// actions/posts.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function likePost(postId: string) {
const post = await db.post.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
revalidatePath(`/posts/${postId}`);
return post.likes;
}
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
revalidatePath('/posts');
redirect('/posts');
}组合使用
jsx
// app/posts/[id]/page.tsx — Server Component
import { db } from '@/lib/db';
import { LikeButton } from '@/components/LikeButton';
import { CommentSection } from '@/components/CommentSection';
export default async function PostPage({ params }) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Server Component 渲染的静态内容 + Client Component 的交互部分 */}
<LikeButton postId={post.id} initialLikes={post.likes} />
<Suspense fallback={<CommentsSkeleton />}>
<CommentSection postId={post.id} />
</Suspense>
</article>
);
}二、Server vs Client Component 规则
什么时候用 Server Component
- 数据获取(数据库、文件系统、内部 API)
- 访问后端资源(环境变量、密钥)
- 大型依赖(markdown 渲染器、语法高亮库 —— 不计入客户端 bundle)
- 纯展示(无交互、无状态)
什么时候用 Client Component
- 交互:onClick、onChange、onSubmit 等事件处理
- 状态:useState、useReducer
- 副作用:useEffect(浏览器 API、订阅、定时器)
- 浏览器 API:localStorage、window、navigator
- 自定义 Hooks(使用了以上功能的)
组合规则
✅ Server → Server 直接 import
✅ Server → Client 直接 import
❌ Client → Server 不能直接 import
✅ Client ← Server 通过 children/props 接收
关键理解:'use client' 是一个"边界"
从标记处开始,该文件及其 import 的所有模块都是 Client Component
但通过 props 传入的 JSX 仍然可以是 Server Componentjsx
// ✅ Server Component 通过 children 传入 Client Component
// layout.tsx (Server Component)
import { AnimatedContainer } from './AnimatedContainer';
import { ServerData } from './ServerData';
export default function Layout({ children }) {
return (
<AnimatedContainer>
<ServerData /> {/* Server Component 作为 children 传入 */}
{children}
</AnimatedContainer>
);
}
// AnimatedContainer.tsx
'use client';
import { motion } from 'framer-motion';
export function AnimatedContainer({ children }) {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{children} {/* 这里渲染的是服务端已序列化的内容 */}
</motion.div>
);
}三、RSC 协议与传输格式
RSC 渲染流程
1. 客户端发起请求(首次加载或导航)
2. 服务端接收请求:
a. 执行 Server Component 树
b. 遇到 Client Component → 生成占位引用(不执行)
c. 将结果序列化为 RSC Payload(流式 JSON)
3. 传输 RSC Payload 到客户端
4. 客户端接收:
a. 解析 RSC Payload
b. 对 Server Component 部分直接渲染为 React 元素
c. 对 Client Component 引用加载对应 JS 并渲染
d. 只有 Client Component 需要水合RSC Payload 格式(简化)
RSC Payload 是一种流式 JSON 格式,每行一个 chunk:
0: ["$", "div", null, {"children": [
["$", "h1", null, {"children": "文章标题"}],
["$", "$L1", null, {"postId": "123", "initialLikes": 42}]
]}]
解读:
- "$" 表示 React 元素
- "div"、"h1" 是 HTML 标签(Server Component 渲染结果)
- "$L1" 表示 Client Component 引用(L = lazy,1 = chunk ID)
- postId、initialLikes 是传给 Client Component 的 props
- Client Component 的代码通过单独的 JS chunk 加载
关键:Server Component 的代码(数据库查询等)不在 payload 中
只有渲染结果(HTML 结构 + 文本)被传输首次加载 vs 客户端导航
首次加载(Full Page Load):
服务端 → HTML(包含 Server Component 渲染的内容)
→ RSC Payload(内嵌在 HTML 的 script 标签中)
→ Client Component JS(独立 script)
客户端 → 渲染 HTML → 加载 JS → 水合 Client Component
客户端导航(Client Navigation):
客户端 → fetch RSC Payload(只请求新路由的数据)
服务端 → 执行新路由的 Server Component → 流式返回 RSC Payload
客户端 → 用新的 RSC Payload 更新虚拟 DOM → 只更新变化的部分
→ 不需要完整的 HTML,不需要全量水合四、缓存机制(Next.js)
四层缓存
请求链路中的缓存层级:
1. 请求去重(Request Memoization)
同一次渲染中,多个组件请求相同 URL → 自动去重,只发一次
仅在 Server Component 的单次渲染周期内有效
2. 数据缓存(Data Cache)
fetch 请求的结果缓存在服务端
默认缓存(可通过 revalidate 控制失效时间)
持久化跨请求、跨部署
3. 整页缓存(Full Route Cache)
静态路由的 RSC Payload + HTML 缓存在服务端
动态路由(使用 cookies/headers/searchParams)不缓存
4. 路由缓存(Router Cache)
客户端缓存已访问过的路由
前进/后退时直接使用,无需重新请求缓存控制
jsx
// 1. fetch 级别的缓存控制
// 默认缓存
const data = await fetch('https://api.example.com/data');
// 不缓存
const data = await fetch('https://api.example.com/data', { cache: 'no-store' });
// 定时重验证(ISR - Incremental Static Regeneration)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // 1小时后重新验证
});
// 按标签重验证
const data = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
});
// 然后在 Server Action 中
import { revalidateTag } from 'next/cache';
revalidateTag('posts'); // 使所有带 'posts' 标签的缓存失效
// 2. 路由级别
import { revalidatePath } from 'next/cache';
revalidatePath('/posts'); // 重验证特定路径
revalidatePath('/posts', 'layout'); // 重验证整个布局
// 3. 整页动态渲染
export const dynamic = 'force-dynamic'; // 每次请求都重新渲染
export const revalidate = 60; // ISR:60秒重验证五、Server Components vs 传统 SSR
| 维度 | 传统 SSR | RSC |
|---|---|---|
| JS 到客户端 | 全部组件的 JS | 只有 Client Component |
| 水合 | 全量水合 | 只水合 Client Component |
| 数据获取 | getServerSideProps 等 | 组件内直接 await |
| 运行时机 | 请求时渲染一次 | 可流式、可缓存 |
| 组件交互 | 水合后所有组件可交互 | SC 无交互,CC 水合后可交互 |
| bundle 大小 | 全量 | 显著减小 |
| 后续导航 | 客户端渲染或重新 SSR | RSC Payload 增量更新 |
六、常见模式与最佳实践
数据获取模式
jsx
// ✅ 并行数据获取
async function Dashboard() {
// Promise.all 并行请求,而非串行 await
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats(),
]);
return (
<div>
<UserCard user={user} />
<PostList posts={posts} />
<StatsChart stats={stats} />
</div>
);
}
// ✅ Suspense 实现渐进式加载
async function Dashboard() {
const user = await getUser(); // 关键数据先获取
return (
<div>
<UserCard user={user} />
<Suspense fallback={<PostsSkeleton />}>
<PostList /> {/* 内部自己 await,独立 Suspense */}
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsChart />
</Suspense>
</div>
);
}序列化边界
jsx
// Server → Client 传递的 props 必须可序列化
// ✅ 可传递
<ClientComponent
name="张三" // string
count={42} // number
active={true} // boolean
tags={['react']} // 数组
config={{ key: 'v' }} // 普通对象
date="2026-03-25" // 日期转为字符串
/>
// ❌ 不可传递
<ClientComponent
onClick={() => {}} // 函数
ref={myRef} // Ref 对象
date={new Date()} // Date 实例
map={new Map()} // Map/Set
element={<div />} // React 元素(可通过 children 传)
/>
// Server Action 是特殊的可序列化函数引用
// ✅ 可以传递 Server Action
import { deletePost } from '@/actions';
<DeleteButton action={deletePost} postId={post.id} />环境隔离
jsx
// 防止服务端代码意外被 Client Component 导入
// lib/db.ts
import 'server-only'; // 如果被客户端导入会报错
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
// 防止客户端代码被服务端导入
// utils/analytics.ts
import 'client-only';
export function trackEvent(name: string) {
window.gtag('event', name);
}七、RSC 在非 Next.js 框架中的使用
RSC 是 React 的特性,不是 Next.js 独有的。
但目前完整实现 RSC 需要框架支持:
已支持:
- Next.js App Router(最成熟)
- Remix / React Router v7(实验性)
- Waku(轻量 RSC 框架)
原因:RSC 需要:
1. 构建工具支持(区分 Server/Client 模块图)
2. 服务端运行时(执行 Server Component)
3. 流式传输协议(RSC Payload)
4. 客户端运行时(解析 Payload、加载 Client Component)
不推荐自行实现,使用框架提供的集成方案。面试高频题
Q: Server Components 和 SSR 有什么区别?
- SSR:在服务端渲染 HTML,发送全部 JS 到客户端进行水合。所有组件代码都在 bundle 中
- RSC:Server Component 只在服务端执行,代码永远不到客户端,不需要水合。只有 Client Component 需要发送 JS 和水合
- SSR 是"在服务端运行一次,然后客户端再运行一次";RSC 是"在服务端运行,永不到客户端"
Q: 'use client' 是什么意思?
'use client' 不是"这个组件在客户端渲染"的意思,而是标记一个边界。从这个文件开始,它及其导入的所有模块都会被包含在客户端 bundle 中。没有标记的组件默认是 Server Component。
Q: Server Component 可以使用 useState 吗?
不可以。Server Component 在服务端执行一次后不再运行,没有"重渲染"的概念。所有需要状态、副作用、事件处理的逻辑必须放在 Client Component 中。
Q: 如何在 Client Component 中使用 Server Component?
不能在 Client Component 中 import Server Component。但可以通过 children 或 props 的方式将 Server Component 的渲染结果传递给 Client Component。这是因为 Server Component 的渲染结果已经被序列化为 RSC Payload,可以作为 props 传递。