Skip to content

深入理解 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,加速首屏

作用

  1. 零客户端 JS:Server Component 的代码完全在服务端执行
  2. 直接数据访问:消除 API 层,减少请求往返
  3. 自动代码分割:Client Component 的 import 自动变成代码分割点
  4. 面试新热点: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 Component
jsx
// ✅ 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

维度传统 SSRRSC
JS 到客户端全部组件的 JS只有 Client Component
水合全量水合只水合 Client Component
数据获取getServerSideProps 等组件内直接 await
运行时机请求时渲染一次可流式、可缓存
组件交互水合后所有组件可交互SC 无交互,CC 水合后可交互
bundle 大小全量显著减小
后续导航客户端渲染或重新 SSRRSC 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。但可以通过 childrenprops 的方式将 Server Component 的渲染结果传递给 Client Component。这是因为 Server Component 的渲染结果已经被序列化为 RSC Payload,可以作为 props 传递。