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。但可以通过 children 或 props 的方式将 Server Component 的渲染结果传递给 Client Component。这是因为 Server Component 的渲染结果已经被序列化为 RSC Payload,可以作为 props 传递。