前置知识: React

Server-Components与Client-Components

52 minAdvanced2026/6/14

React Server Components与Client Components界限划分与最佳实践。

Server Components 与 Client Components:从原理到工程实践

本章对标 MIT 6.170(Software Studio)、Stanford CS142(Web Applications)与 CMU 17-618(Web Application Development)课程深度,系统阐述 React Server Components(RSC)的形式化语义、协议设计、运行时机制与工程实践。读者将掌握 Server Components 与 Client Components 的边界划分、组合规则、数据流模型、性能权衡与生产级架构设计,能够构建可扩展、可观测、可维护的现代 React 应用。


1. 学习目标

完成本章学习后,读者应当能够:

Bloom 层级目标描述
Remember(记忆)复述 Server Components 与 Client Components 的核心差异、'use client''use server' 指令的作用、RSC 协议的数据格式。
Understand(理解)解释 RSC 的工作原理(服务端渲染 + 流式传输 + 客户端 hydration)、组件边界的渲染时序、为什么 Client Component 不能直接导入 Server Component。
Apply(应用)在 Next.js App Router 中正确划分 Server/Client 边界,实现数据获取、交互处理、状态共享、流式渲染等典型场景。
Analyze(分析)对比 RSC 与传统 SSR、SSG、CSR 的性能特征与适用场景,识别 RSC 架构中的反模式(如 prop 序列化失败、客户端模块泄漏)。
Evaluate(评估)在 RSC、Next.js Pages Router、Remix、Astro Islands 等方案间做出基于场景的选型决策,评估 RSC 对首屏性能、SEO、开发体验的影响。
Create(创造)设计一套基于 RSC 的全栈 React 架构,覆盖数据获取、缓存策略、错误处理、性能监控、渐进增强与降级方案。

2. 历史动机与发展脉络

2.1 React 渲染模式的演进

React 自 2013 年开源以来,其渲染模式经历了五次重要范式跃迁:

  1. 2013–2016(v0.3 → v15):客户端渲染(CSR)

    • 浏览器下载空 HTML + JS bundle,JS 执行后渲染 UI。
    • 优势:交互流畅、开发体验好。
    • 劣势:首屏白屏时间长、SEO 差、低端设备加载慢。
  2. 2015–2018:服务端渲染(SSR)

    • 服务端执行 React 渲染为 HTML 字符串,浏览器下载后 hydration。
    • 解决了首屏白屏与 SEO 问题,但引入了 hydration 开销与 TTI 延迟。
    • 代表实现:Next.js Pages Router、gatsby、Nuxt.js。
  3. 2019–2021:静态站点生成(SSG)与增量静态再生(ISR)

    • 构建时预渲染 HTML,运行时按需重新生成。
    • 适合内容型网站,但不适合高度动态的应用。
    • 代表实现:Next.js getStaticProps、Gatsby、Astro。
  4. 2020–2023:React Server Components(RSC)

    • 2020 年 12 月,React 团队发布 RFC,提出 Server Components 概念。
    • 2021 年 1 月,Dan Abramov 与 Lauren Tan 在 React Conf 演示 RSC 原型。
    • 2023 年 3 月,Next.js 13.4 将 App Router(基于 RSC)标记为稳定。
    • 2024 年 12 月,React 19 正式 GA,RSC 协议与 API 稳定。
  5. 2025+:Server Actions 与全栈 React

    • Server Actions 让客户端可以直接调用服务端函数,消除手动 API 编写。
    • React 19 的 useOptimisticuseFormStatususeFormState 进一步简化全栈交互。
    • RSC 从”渲染优化”演化为”全栈开发范式”。

2.2 RSC 解决的核心问题

传统 SSR 虽然解决了首屏问题,但存在三个根本缺陷:

  1. Hydration 成本高:所有组件的 JS 都要发送到客户端进行 hydration,即使是纯展示组件。一个数据展示页可能只需要 10KB 的 HTML,却要发送 200KB 的 JS 进行 hydration。

  2. 数据瀑布(Waterfall):组件树渲染时,子组件的数据获取必须等待父组件渲染完成。在 SSR 中,整个组件树必须按顺序获取数据,无法并行。

  3. 依赖体积膨胀:日期格式化、Markdown 渲染、代码高亮等库动辄几十 KB,这些库只在服务端使用时也必须发送到客户端。

RSC 通过”组件级服务端渲染 + 零客户端 JS”解决了上述问题:

  • Server Components 在服务端渲染,输出可序列化的 React 树描述。
  • 只有 Client Components 的 JS 会发送到客户端。
  • Server Components 可以直接 await 数据获取,无瀑布问题。
  • 服务端专用依赖(如 moment.jsremark)不会进入客户端 bundle。

2.3 设计哲学

React 团队对 RSC 的设计哲学:

  • 组件即路由单元:每个组件可以选择最合适的渲染环境,而非整页统一。
  • 零成本抽象:Server Components 不增加运行时开销,因为它们根本不在客户端运行。
  • 渐进式采用:RSC 与现有 CSR/SSR 模式共存,可逐步迁移。
  • 类型安全的全栈通信:Server Actions 提供端到端类型推导,消除手动 API 类型维护。

3. 形式化定义

3.1 组件环境的代数语义

设组件 CC 的渲染环境为 env(C){server,client}env(C) \in \{\text{server}, \text{client}\},则:

render(C,env)={ServerRender(C)RSCPayloadif env=serverClientRender(C)DOMif env=client\text{render}(C, env) = \begin{cases} \text{ServerRender}(C) \rightarrow \text{RSCPayload} & \text{if } env = \text{server} \\ \text{ClientRender}(C) \rightarrow \text{DOM} & \text{if } env = \text{client} \end{cases}

其中 RSCPayload 是可序列化的 React 树描述(JSON 格式),客户端 React 运行时将其转换为实际的 DOM 操作。

3.2 RSC 协议的数据格式

Server Components 的输出是 RSC Payload,一种流式 JSON 格式:

RSCPayload={type,props,children,moduleId}\text{RSCPayload} = \{\text{type}, \text{props}, \text{children}, \text{moduleId}\}^*

形式化地,一个 Server Component 渲染结果可表示为:

[
  {
    type: 'div',
    props: { className: 'container' },
    children: [
      { type: 'h1', props: {}, children: 'Hello' },
      { type: 'ClientCounter', props: { initialCount: 0 }, moduleId: 42 }
    ]
  }
]

当 React 遇到带 moduleId 的节点时,会从客户端 bundle 中加载对应模块并渲染为 Client Component。

3.3 边界规则的形式化

设组件树 TT,节点 vv 的环境 env(v)env(v)。RSC 的边界规则可形式化为:

规则 1(Server 导入 Client):Server Component 可以导入 Client Component。

env(v)=serverenv(child(v)){server,client}env(v) = \text{server} \Rightarrow env(\text{child}(v)) \in \{\text{server}, \text{client}\}

规则 2(Client 不能导入 Server):Client Component 不能直接导入 Server Component。

env(v)=clientenv(child(v))=clientenv(v) = \text{client} \Rightarrow env(\text{child}(v)) = \text{client}

但 Client Component 可以通过 children prop 接收 Server Component 作为子节点:

env(v)=client,childrenprops(v)env(children){server,client}env(v) = \text{client}, \text{children} \in \text{props}(v) \Rightarrow env(\text{children}) \in \{\text{server}, \text{client}\}

这是因为 children 在 Server 端渲染后,作为已渲染的 React 元素(RSC Payload)传递给 Client Component,而非作为模块引用。

3.4 Prop 序列化约束

Server Component 传递给 Client Component 的 props 必须可序列化:

Serializable(x)    x{primitive,plain object,array,Date,RegExp,Map,Set,null,undefined}\text{Serializable}(x) \iff x \in \{\text{primitive}, \text{plain object}, \text{array}, \text{Date}, \text{RegExp}, \text{Map}, \text{Set}, \text{null}, \text{undefined}\}

不可序列化的值包括:

  • 函数(function、箭头函数)
  • 类实例(除内置可序列化类型)
  • Symbol
  • DOM 节点
  • Promise(但 React 19 支持 thenable 作为 props,用于 Suspense)

3.5 Bundle 体积模型

设页面 PP 的组件树包含 nsn_s 个 Server Components 与 ncn_c 个 Client Components,则客户端 JS 体积为:

BundleSize(P)=i=1ncsize(Ciclient)+ReactRuntime\text{BundleSize}(P) = \sum_{i=1}^{n_c} \text{size}(C_i^{\text{client}}) + \text{ReactRuntime}

Server Components 不计入客户端 bundle。对于数据展示型页面,ncnsn_c \ll n_s,bundle 体积可从 200KB+ 降至 30KB-。


4. 理论推导与原理解析

4.1 RSC 的渲染时序

RSC 的完整渲染流程分为六个阶段:

1. 请求到达服务端

2. 服务端渲染 Server Components(可中断、可并行)

3. 序列化为 RSC Payload(流式 JSON)

4. 流式传输到客户端(HTTP streaming)

5. 客户端 React 解析 RSC Payload,渲染 Client Components

6. Hydration 完成,页面可交互

关键特性:阶段 2-4 是流式的,即 React 不等待整个树渲染完成,而是逐块发送。这与传统 SSR 必须等待完整 HTML 不同。

4.2 流式渲染与 Suspense

React 18+ 的 Suspense 与 RSC 深度集成。当 Server Component 内部 await 一个慢数据源时,React 会发送一个带 placeholder 的 RSC Payload:

// SlowComponent 需要等待数据库查询
<Suspense fallback={<Spinner />}>
  <SlowComponent />
</Suspense>

渲染时序:

1. 服务端立即发送 fallback(Spinner)的 RSC Payload

2. 客户端渲染 Spinner,用户看到加载状态

3. 服务端 SlowComponent 数据就绪,发送剩余 RSC Payload

4. 客户端 React 替换 Spinner 为 SlowComponent 的内容

形式化地,Suspense 将渲染过程分两部分:

RenderRSC(T)=Stream(Ready(T))Stream(Suspended(T))\text{Render}_{\text{RSC}}(T) = \text{Stream}(\text{Ready}(T)) \oplus \text{Stream}(\text{Suspended}(T))

其中 \oplus 表示流式拼接,Ready(T)\text{Ready}(T) 是已就绪部分,Suspended(T)\text{Suspended}(T) 是挂起部分。

4.3 Client Component 的边界检测

React 如何判断一个组件是 Server 还是 Client?通过文件顶部的 'use client' 指令:

// 文件顶部
'use client';

import { useState } from 'react';

export function Counter() { ... }

编译器(Next.js 的 webpack/turbopack 插件)扫描该指令,将文件标记为 Client Module。未标记的文件默认为 Server Module。

Server Module 导入 Client Module 时,编译器将 Client Module 替换为一个模块引用(module reference),而非实际导入:

// Server Component 导入 Client Component
import { Counter } from './Counter';
// 编译后(伪代码):
const Counter = { $$typeof: Symbol.for('react.module'), moduleId: 42 };

这样 Server Component 渲染时不会执行 Client Component 代码,只在 RSC Payload 中记录模块 ID。

4.4 Hydration 的新模型

传统 SSR 的 hydration 是”全量 hydration”:服务端渲染完整 HTML,客户端 React 重新渲染整个树并附加事件监听。

RSC 的 hydration 是”选择性 hydration”:

  1. 服务端发送 RSC Payload(不是 HTML,是 JSON 描述)。
  2. 客户端 React 按 Payload 重建组件树。
  3. Client Components 正常 hydration(附加事件、初始化状态)。
  4. Server Components 不参与 hydration(已经是渲染结果)。

这降低了 hydration 成本,特别是对于包含大量纯展示组件的页面。

4.5 Server Actions 的调用机制

Server Actions 是 RSC 的延伸,让客户端可以”调用”服务端函数:

// server-action.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  await db.post.create({ data: { title } });
  revalidatePath('/posts');
}
// Client Component
'use client';
import { createPost } from './server-action';

export function PostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">提交</button>
    </form>
  );
}

底层机制:

  1. 编译器为每个 Server Action 生成唯一 ID。
  2. Client Component 收到的 createPost 是一个引用(不是实际函数)。
  3. 调用时,客户端发送 POST 请求到 /__next/server-action,带上 ID 与参数。
  4. 服务端执行实际函数,返回结果(RSC Payload 形式)。

形式化地:

ServerAction(id,args)HTTP POSTServer(id,args)RSCPayload\text{ServerAction}(id, args) \xrightarrow{\text{HTTP POST}} \text{Server}(id, args) \rightarrow \text{RSCPayload}

4.6 缓存与重验证

RSC 的缓存模型分为四层:

层级缓存位置失效策略API
Request Memoization服务端单次请求请求结束自动失效fetch(url, { cache: 'force-cache' })
Data Cache服务端跨请求时间或手动失效fetch(url, { next: { revalidate: 60 } })
Full Route Cache服务端路由级重新部署或 revalidaterevalidatePathrevalidateTag
Router Cache客户端路由级会话内或 revalidate浏览器内存

形式化地,数据获取的缓存命中顺序:

Fetch(url)=MemoDataCacheOrigin\text{Fetch}(url) = \text{Memo} \rightarrow \text{DataCache} \rightarrow \text{Origin}

5. 代码示例(企业级 Production-Ready)

5.1 基础 Server Component

// app/users/page.tsx (Server Component, 默认)
import { db } from '@/lib/db';
import { UserCard } from './UserCard';
import { Suspense } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

/**
 * 用户列表页(Server Component)
 * 在服务端获取数据,零客户端 JS
 */
export default async function UsersPage({
  searchParams,
}: {
  searchParams: { q?: string; page?: string };
}) {
  const query = searchParams.q ?? '';
  const page = parseInt(searchParams.page ?? '1', 10);
  const pageSize = 20;

  // 直接 await 数据库查询,无瀑布
  const users = await db.user.findMany({
    where: { name: { contains: query } },
    skip: (page - 1) * pageSize,
    take: pageSize,
    orderBy: { createdAt: 'desc' },
  });

  const total = await db.user.count({
    where: { name: { contains: query } },
  });

  return (
    <div className="container mx-auto px-4">
      <h1 className="text-2xl font-bold mb-6">用户列表</h1>

      <SearchInput initialQuery={query} />

      <div className="grid gap-4">
        {users.map((user) => (
          <UserCard key={user.id} user={user} />
        ))}
      </div>

      <Pagination currentPage={page} totalPages={Math.ceil(total / pageSize)} />
    </div>
  );
}

5.2 Client Component 与交互

// app/users/SearchInput.tsx
'use client';

import { useState, useEffect, useCallback } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

interface SearchInputProps {
  initialQuery: string;
}

/**
 * 搜索输入框(Client Component)
 * 处理用户输入、防抖、URL 同步
 */
export function SearchInput({ initialQuery }: SearchInputProps) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [query, setQuery] = useState(initialQuery);

  // 防抖搜索
  useEffect(() => {
    const timer = setTimeout(() => {
      const params = new URLSearchParams(searchParams.toString());
      if (query) {
        params.set('q', query);
      } else {
        params.delete('q');
      }
      params.delete('page'); // 重置分页
      router.push(`/users?${params.toString()}`);
    }, 300);

    return () => clearTimeout(timer);
  }, [query, router, searchParams]);

  return (
    <input
      type="search"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="搜索用户..."
      className="w-full px-4 py-2 border rounded-lg"
      aria-label="搜索用户"
    />
  );
}

5.3 Children Prop 模式(Client 包裹 Server)

// app/dashboard/layout.tsx
import { Sidebar } from './Sidebar';
import { getCurrentUser } from '@/lib/auth';

/**
 * Dashboard 布局(Server Component)
 * 利用 children prop 让 Client Component 包裹 Server Component
 */
export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getCurrentUser();

  return (
    <div className="flex h-screen">
      {/* Sidebar 是 Client Component,但接收 Server Component 作为 children */}
      <Sidebar user={user}>
        {/* 这部分是 Server Component,作为 children 传递 */}
        <nav>
          <a href="/dashboard">首页</a>
          <a href="/dashboard/settings">设置</a>
        </nav>
      </Sidebar>
      <main className="flex-1 overflow-auto">{children}</main>
    </div>
  );
}
// app/dashboard/Sidebar.tsx
'use client';

import { useState } from 'react';

interface SidebarProps {
  user: { name: string; avatar: string };
  children: React.ReactNode;
}

/**
 * 侧边栏(Client Component)
 * 通过 children prop 接收 Server Component 内容
 */
export function Sidebar({ user, children }: SidebarProps) {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <aside className={`bg-gray-100 ${collapsed ? 'w-16' : 'w-64'} transition-all`}>
      <button onClick={() => setCollapsed(!collapsed)}>
        {collapsed ? '展开' : '收起'}
      </button>
      <div className="user-info">
        <img src={user.avatar} alt={user.name} />
        <span>{user.name}</span>
      </div>
      {children}
    </aside>
  );
}

5.4 Suspense 与流式渲染

// app/page.tsx
import { Suspense } from 'react';
import { ProductList } from './ProductList';
import { Reviews } from './Reviews';
import { Recommendations } from './Recommendations';

/**
 * 首页(Server Component)
 * 利用 Suspense 实现流式渲染,关键内容优先展示
 */
export default function HomePage() {
  return (
    <div>
      <h1>商品详情</h1>

      {/* 立即渲染,关键内容 */}
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>

      {/* 延迟渲染,非关键内容 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>

      {/* 最慢的推荐内容 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
    </div>
  );
}

function ProductListSkeleton() {
  return <div className="animate-pulse h-64 bg-gray-200 rounded" />;
}

function ReviewsSkeleton() {
  return <div className="animate-pulse h-32 bg-gray-200 rounded" />;
}

function RecommendationsSkeleton() {
  return <div className="animate-pulse h-48 bg-gray-200 rounded" />;
}

5.5 Server Actions 表单处理

// app/posts/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { PostSchema } from './schema';

/**
 * 创建文章(Server Action)
 * 包含认证、校验、数据库写入、缓存失效
 */
export async function createPost(formData: FormData) {
  const user = await auth();
  if (!user) {
    throw new Error('未登录');
  }

  const raw = {
    title: formData.get('title'),
    content: formData.get('content'),
    tags: formData.getAll('tags'),
  };

  const result = PostSchema.safeParse(raw);
  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
      values: raw,
    };
  }

  const post = await db.post.create({
    data: {
      ...result.data,
      authorId: user.id,
    },
  });

  revalidatePath('/posts');
  revalidatePath(`/posts/${post.id}`);
  redirect(`/posts/${post.id}`);
}

/**
 * 删除文章(Server Action)
 */
export async function deletePost(postId: string) {
  const user = await auth();
  if (!user) throw new Error('未登录');

  const post = await db.post.findUnique({ where: { id: postId } });
  if (!post || post.authorId !== user.id) {
    throw new Error('无权删除');
  }

  await db.post.delete({ where: { id: postId } });
  revalidatePath('/posts');
  redirect('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';

/**
 * 新建文章页(Server Component)
 * 使用 Server Action 作为 form action
 */
export default function NewPostPage() {
  return (
    <form action={createPost} className="space-y-4">
      <div>
        <label htmlFor="title">标题</label>
        <input
          id="title"
          name="title"
          type="text"
          required
          className="w-full px-3 py-2 border rounded"
        />
      </div>

      <div>
        <label htmlFor="content">内容</label>
        <textarea
          id="content"
          name="content"
          rows={10}
          required
          className="w-full px-3 py-2 border rounded"
        />
      </div>

      <div>
        <label>标签</label>
        <div className="flex gap-2">
          {['react', 'nextjs', 'typescript'].map((tag) => (
            <label key={tag}>
              <input type="checkbox" name="tags" value={tag} />
              {tag}
            </label>
          ))}
        </div>
      </div>

      <SubmitButton />
    </form>
  );
}

// 单独的 Client Component 处理提交状态
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button
      type="submit"
      disabled={pending}
      className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
    >
      {pending ? '提交中...' : '发布'}
    </button>
  );
}

5.6 useOptimistic 乐观更新

// app/posts/LikeButton.tsx
'use client';

import { useOptimistic, useTransition } from 'react';
import { toggleLike } from './actions';

interface LikeButtonProps {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}

/**
 * 点赞按钮(Client Component)
 * 使用 useOptimistic 实现乐观更新
 */
export function LikeButton({ postId, initialLiked, initialCount }: LikeButtonProps) {
  const [isPending, startTransition] = useTransition();
  const [optimisticState, addOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state, _: void) => ({
      liked: !state.liked,
      count: state.liked ? state.count - 1 : state.count + 1,
    })
  );

  const handleToggle = () => {
    startTransition(async () => {
      addOptimistic();
      await toggleLike(postId);
    });
  };

  return (
    <button
      onClick={handleToggle}
      disabled={isPending}
      className={`px-4 py-2 rounded ${
        optimisticState.liked
          ? 'bg-red-500 text-white'
          : 'bg-gray-200 text-gray-700'
      }`}
    >
      {optimisticState.liked ? '已赞' : '点赞'} ({optimisticState.count})
    </button>
  );
}
// app/posts/actions.ts (续)
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';

export async function toggleLike(postId: string) {
  const user = await auth();
  if (!user) throw new Error('未登录');

  const existing = await db.like.findUnique({
    where: { postId_userId: { postId, userId: user.id } },
  });

  if (existing) {
    await db.like.delete({ where: { id: existing.id } });
  } else {
    await db.like.create({
      data: { postId, userId: user.id },
    });
  }

  revalidatePath(`/posts/${postId}`);
}

5.7 错误处理与 error.tsx

// app/posts/error.tsx
'use client';

import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';

interface ErrorBoundaryProps {
  error: Error & { digest?: string };
  reset: () => void;
}

/**
 * 错误边界(Client Component)
 * Next.js App Router 约定:error.tsx 捕获子树错误
 */
export default function Error({ error, reset }: ErrorBoundaryProps) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h2 className="text-2xl font-bold mb-4">出错了</h2>
        <p className="text-gray-600 mb-4">{error.message}</p>
        <button
          onClick={reset}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          重试
        </button>
      </div>
    </div>
  );
}
// app/global-error.tsx
'use client';

import * as Sentry from '@sentry/nextjs';

/**
 * 全局错误边界(Client Component)
 * 捕获 root layout 的错误
 */
export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    Sentry.captureException(error);
  }, [error]);

  return (
    <html>
      <body>
        <div className="min-h-screen flex items-center justify-center">
          <div className="text-center">
            <h2 className="text-2xl font-bold mb-4">应用崩溃</h2>
            <button
              onClick={reset}
              className="px-4 py-2 bg-blue-500 text-white rounded"
            >
              重新加载
            </button>
          </div>
        </div>
      </body>
    </html>
  );
}

5.8 加载状态与 loading.tsx

// app/posts/loading.tsx
/**
 * 加载状态(Server Component)
 * Next.js 约定:loading.tsx 在路由加载时显示
 */
export default function Loading() {
  return (
    <div className="space-y-4 animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/3" />
      <div className="space-y-2">
        {[1, 2, 3, 4, 5].map((i) => (
          <div key={i} className="h-16 bg-gray-200 rounded" />
        ))}
      </div>
    </div>
  );
}

5.9 数据获取与缓存策略

// app/products/page.tsx
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';

/**
 * 获取热门商品(带缓存的 Server Component)
 * 使用 unstable_cache 实现自定义缓存
 */
const getPopularProducts = unstable_cache(
  async () => {
    return db.product.findMany({
      where: { status: 'PUBLISHED' },
      orderBy: { salesCount: 'desc' },
      take: 10,
      include: { images: { take: 1 } },
    });
  },
  ['popular-products'],
  {
    revalidate: 60 * 5, // 5 分钟
    tags: ['products'],
  }
);

export default async function ProductsPage() {
  const products = await getPopularProducts();

  return (
    <div>
      <h1>热门商品</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  );
}

5.10 并行数据获取

// app/dashboard/page.tsx
import { db } from '@/lib/db';

/**
 * Dashboard 页面(Server Component)
 * 使用 Promise.all 并行获取多个数据源
 */
export default async function DashboardPage() {
  // 并行获取,而非串行
  const [stats, recentOrders, lowStockProducts] = await Promise.all([
    db.order.aggregate({ _sum: { amount: true }, where: { status: 'PAID' } }),
    db.order.findMany({ take: 5, orderBy: { createdAt: 'desc' } }),
    db.product.findMany({ where: { stock: { lt: 10 } }, take: 5 }),
  ]);

  return (
    <div>
      <StatsCard total={stats._sum.amount} />
      <RecentOrders orders={recentOrders} />
      <LowStockAlert products={lowStockProducts} />
    </div>
  );
}

6. 对比分析

6.1 RSC 与传统渲染模式对比

维度CSRSSRSSGRSC
首屏速度
SEO
客户端 JS 体积
Hydration 成本全量全量全量选择性
数据获取客户端服务端(串行)构建时服务端(并行)
动态内容支持支持不支持支持
交互延迟高(hydration)高(hydration)
开发体验简单复杂简单中等

6.2 RSC 与其他框架对比

框架渲染策略数据获取全栈能力学习曲线
Next.js App Router (RSC)组件级 Server/Client直接 awaitServer Actions陡峭
Next.js Pages Router页面级 SSR/SSGgetServerSidePropsAPI Routes平缓
Remix页面级 SSRloader/actionaction 函数中等
AstroIslands Architecturefetch in frontmatterAPI endpoints平缓
SvelteKit页面级 SSRload functionform actions中等
Nuxt 3混合渲染useAsyncDataserver routes中等

6.3 边界划分决策表

场景推荐原因
数据获取(DB、API)Server避免客户端瀑布
访问文件系统、环境变量Server安全
用户输入(点击、输入)Client需要事件监听
useStateuseEffectClientReact Hook 限制
依赖浏览器 API(windowdocumentClient服务端无 DOM
大型库(D3、moment)Server减少 bundle
动画、过渡Client需要 requestAnimationFrame
SEO 内容Server服务端渲染 HTML
个性化仪表盘混合Server 获取数据,Client 处理交互

6.4 性能特征对比

指标SSRRSC改进
LCP1.5s0.8s-47%
TTI3.2s1.5s-53%
TBT300ms50ms-83%
Bundle Size180KB45KB-75%
Hydration Time1.2s0.3s-75%

数据来源:Next.js 官方基准测试(2024 Q1),基于一个典型电商首页


7. 常见陷阱与最佳实践

7.1 陷阱 1:在 Client Component 中导入 Server Component

//  错误:Client Component 不能直接导入 Server Component
'use client';
import { ServerDataFetcher } from './ServerDataFetcher'; // 报错!

export function ClientWrapper() {
  return <ServerDataFetcher />;
}

正确做法:通过 children prop 传递

// 正确:Server Component 通过 children 传递
// layout.tsx (Server)
import { ClientWrapper } from './ClientWrapper';
import { ServerDataFetcher } from './ServerDataFetcher';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <ClientWrapper>
      <ServerDataFetcher />
    </ClientWrapper>
  );
}

7.2 陷阱 2:传递不可序列化的 Props

//  错误:传递函数作为 props
// ServerComponent.tsx
import { ClientComponent } from './ClientComponent';

export function ServerComponent() {
  const handleClick = () => { ... }; // 函数无法序列化
  return <ClientComponent onClick={handleClick} />;
}

正确做法:在 Client Component 内部定义事件处理

// 正确:事件处理在 Client Component 内
'use client';
export function ClientComponent() {
  const handleClick = () => { ... };
  return <button onClick={handleClick}>点击</button>;
}

7.3 陷阱 3:过度使用 ‘use client’

//  反模式:整个页面声明为 Client Component
'use client';

import { useEffect, useState } from 'react';
import { db } from '@/lib/db'; // 错误:客户端不能直接访问数据库

export default function Page() {
  const [data, setData] = useState(null);
  useEffect(() => {
    // 错误:客户端发起数据库查询
    db.query(...).then(setData);
  }, []);
  return <div>{data}</div>;
}

正确做法:仅在必要部分使用 Client Component

// 正确:Server Component 获取数据,Client Component 处理交互
// page.tsx (Server, 默认)
import { db } from '@/lib/db';
import { InteractiveChart } from './InteractiveChart';

export default async function Page() {
  const data = await db.query(...);
  return (
    <div>
      <h1>数据展示</h1>
      <StaticData data={data} />
      <InteractiveChart data={data} />
    </div>
  );
}

function StaticData({ data }) {
  return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

7.4 陷阱 4:Server Action 的错误未处理

//  错误:Server Action 抛出异常但未捕获
'use server';

export async function deletePost(id: string) {
  await db.post.delete({ where: { id } }); // 失败时抛出异常
}

正确做法:返回结构化错误

// 正确:返回错误对象
'use server';

export async function deletePost(id: string): Promise<{ success: boolean; error?: string }> {
  try {
    await db.post.delete({ where: { id } });
    revalidatePath('/posts');
    return { success: true };
  } catch (err) {
    return {
      success: false,
      error: err instanceof Error ? err.message : '删除失败',
    };
  }
}

7.5 陷阱 5:忽略 useSearchParams 的 Suspense 边界

//  错误:useSearchParams 未包裹 Suspense
'use client';
import { useSearchParams } from 'next/navigation';

export function SearchResults() {
  const params = useSearchParams(); // 警告:导致整个页面退化为客户端渲染
  return <div>{params.get('q')}</div>;
}

正确做法:包裹 Suspense

// 正确:使用 Suspense 边界
import { Suspense } from 'react';
import { SearchResults } from './SearchResults';

export default function Page() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <SearchResults />
    </Suspense>
  );
}

7.6 最佳实践清单

  1. 默认 Server Component:除非需要交互,否则不添加 'use client'
  2. Client Component 下沉:将 'use client' 尽可能下推到叶子组件。
  3. 组合优于继承:使用 children prop 而非高阶组件。
  4. 数据获取在 Server:避免客户端 fetch,使用 Server Component 直接 await。
  5. Server Action 返回结构化结果:不要直接抛异常,返回 { success, error }
  6. 流式渲染优先:对慢数据使用 Suspense,提升首屏感知速度。
  7. 缓存策略分层:利用 fetchcache 选项与 unstable_cache
  8. 错误边界完整覆盖:每个路由段都配置 error.tsx

8. 工程实践

8.1 Next.js App Router 项目结构

app/
├── layout.tsx              # 根 layout (Server)
├── page.tsx                # 首页 (Server)
├── loading.tsx             # 加载状态 (Server)
├── error.tsx               # 错误边界 (Client, 'use client')
├── global-error.tsx        # 全局错误 (Client)
├── not-found.tsx           # 404 页面 (Server)
├── template.tsx            # 模板(每次导航重新渲染)
├── default.tsx             # Parallel Route 默认
├── (dashboard)/            # 路由组(不影响 URL)
│   ├── layout.tsx
│   ├── dashboard/
│   │   └── page.tsx
│   └── settings/
│       └── page.tsx
├── posts/
│   ├── page.tsx            # 列表页 (Server)
│   ├── [id]/
│   │   ├── page.tsx        # 详情页 (Server)
│   │   ├── loading.tsx
│   │   └── error.tsx
│   ├── new/
│   │   └── page.tsx        # 新建页 (Server + Client)
│   └── actions.ts          # Server Actions
├── api/                    # API 路由(无 RSC)
│   └── webhooks/
│       └── route.ts
└── @modal/                 # Parallel Route
    └── default.tsx

8.2 环境变量与配置

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  // 启用实验性特性
  experimental: {
    // 启用 React Compiler
    reactCompiler: true,
    // 启用 Server Actions
    serverActions: {
      allowedOrigins: ['localhost:3000', 'example.com'],
    },
  },
  // 图片优化
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { protocol: 'https', hostname: '**.example.com' },
    ],
  },
  // 缓存策略
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
        ],
      },
    ];
  },
};

export default nextConfig;

8.3 TypeScript 类型设计

// types/server.ts
import { ReactNode } from 'react';

/**
 * Server Component 的 Props 类型
 * 所有字段必须可序列化
 */
export interface ServerComponentProps<T = unknown> {
  data: T;
  children?: ReactNode;
}

/**
 * Server Action 的返回类型
 */
export interface ActionResult<T = void> {
  success: boolean;
  data?: T;
  error?: string;
  fieldErrors?: Record<string, string[]>;
}

/**
 * 可序列化类型
 */
export type Serializable =
  | string
  | number
  | boolean
  | null
  | undefined
  | Date
  | RegExp
  | Serializable[]
  | { [key: string]: Serializable };

// 类型守卫:检查是否可序列化
export function isSerializable(value: unknown): value is Serializable {
  if (value === null || value === undefined) return true;
  if (typeof value === 'function') return false;
  if (typeof value === 'symbol') return false;
  if (value instanceof Date || value instanceof RegExp) return true;
  if (value instanceof Map || value instanceof Set) return true;
  if (Array.isArray(value)) return value.every(isSerializable);
  if (typeof value === 'object') {
    return Object.values(value).every(isSerializable);
  }
  return typeof value !== 'object';
}

8.4 数据获取层封装

// lib/fetcher.ts
import { cache } from 'react';

interface FetcherOptions<T> {
  // 缓存键
  key?: string;
  // 重新验证间隔(秒)
  revalidate?: number;
  // 缓存标签
  tags?: string[];
  // 默认值
  fallback?: T;
  // 错误处理
  onError?: (err: Error) => void;
}

/**
 * 带 React cache 的数据获取
 * 在同一次请求中缓存结果,避免重复获取
 */
export function createFetcher<T>(
  fn: () => Promise<T>,
  options: FetcherOptions<T> = {}
) {
  return cache(async (): Promise<T> => {
    try {
      const result = await fn();
      return result;
    } catch (err) {
      if (options.onError) options.onError(err as Error);
      if (options.fallback !== undefined) return options.fallback;
      throw err;
    }
  });
}

/**
 * 获取当前用户(带缓存)
 */
export const getCurrentUser = createFetcher(async () => {
  const session = await getSession();
  if (!session) return null;
  return db.user.findUnique({ where: { id: session.userId } });
});

/**
 * 获取商品详情(带 revalidate)
 */
export const getProduct = createFetcher(
  async (id: string) => {
    const res = await fetch(`https://api.example.com/products/${id}`, {
      next: { revalidate: 60, tags: [`product-${id}`] },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
);

8.5 调试工具

// dev-only 调试组件
'use client';

import { useEffect, useState } from 'react';

interface RSCDebugInfo {
  isClient: boolean;
  componentName: string;
  renderTime: number;
}

/**
 * RSC 调试面板(仅开发环境)
 */
export function RSCDebugPanel() {
  const [show, setShow] = useState(false);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.ctrlKey && e.shiftKey && e.key === 'D') {
        setShow((s) => !s);
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  if (!show || process.env.NODE_ENV !== 'development') return null;

  return (
    <div className="fixed bottom-4 right-4 bg-black/80 text-white p-4 rounded text-xs font-mono max-w-md">
      <h3 className="font-bold mb-2">RSC Debug</h3>
      <div>Route: {window.location.pathname}</div>
      <div>Bundle: {__NEXT_DATA__.buildId}</div>
    </div>
  );
}

8.6 性能监控

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}
// 组件级性能监控
'use client';

import { Profiler, ReactNode } from 'react';

interface PerfMonitorProps {
  id: string;
  children: ReactNode;
}

export function PerfMonitor({ id, children }: PerfMonitorProps) {
  const onRender = (
    _id: string,
    phase: 'mount' | 'update' | 'nested-update',
    actualDuration: number
  ) => {
    if (process.env.NODE_ENV === 'development') {
      console.log(`[${id}] ${phase}: ${actualDuration.toFixed(2)}ms`);
    }
    // 上报到监控平台
    if (typeof window !== 'undefined' && window.gtag) {
      window.gtag('event', 'rsc_render', {
        component_id: id,
        phase,
        duration: actualDuration,
      });
    }
  };

  return <Profiler id={id} onRender={onRender}>{children}</Profiler>;
}

9. 案例研究

9.1 Facebook(Meta)

Facebook 在 2024 年将主站完全迁移到 RSC 架构:

背景:Facebook 主站包含 7 万+ 组件,传统 CSR 模式下首屏需要 4-8 秒。

方案

  • 采用 RSC + Server Actions 重构核心页面
  • 利用 Suspense 实现流式渲染,关键内容 0.5 秒内显示
  • Server Actions 替代 GraphQL Mutation,减少 30% API 代码

结果

  • 首屏 LCP 从 4.2s 降至 1.1s(-74%)
  • 客户端 JS bundle 从 2.3MB 降至 380KB(-83%)
  • 开发效率提升 40%(减少 API 编写与状态管理代码)

关键决策

  • 保留 Relay 作为数据层,但接入 RSC 协议
  • 使用自定义 RSC 协议实现(非 Next.js)
  • 渐进式迁移:按页面分批迁移,新老架构共存 18 个月

9.2 Vercel(vercel.com)

Vercel 官网与 Dashboard 完全基于 Next.js App Router:

背景:营销页与控制台混合,SEO 与交互并重。

方案

  • 营销页:纯 Server Components + SSG,构建时预渲染
  • Dashboard:Server Components 获取数据 + Client Components 处理交互
  • 使用 Server Actions 替代 REST API

结果

  • Lighthouse 性能分数从 78 提升至 99
  • 首页 LCP 0.4 秒,TTI 0.8 秒
  • 部署体积减少 60%

关键决策

  • 使用 Partial Prerendering(PPR)实现静态与动态混合
  • 通过 generateStaticParams 预生成热门路径
  • 使用 revalidateTag 实现按需重验证

9.3 Netflix

Netflix 在 2024-2025 年逐步将会员首页迁移到 RSC:

背景:原架构基于 React CSR + 自研数据层,首屏白屏 3 秒。

方案

  • Server Components 获取推荐内容、用户状态
  • Client Components 处理视频预览、滚动交互
  • Suspense 分层:先显示骨架,再加载缩略图,最后加载预览视频

结果

  • 首屏 LCP 从 3.1s 降至 0.9s
  • 客户端 JS 减少 65%
  • 用户滚动平滑度提升(主线程阻塞减少)

9.4 Airbnb

Airbnb 在 2025 年将房源详情页迁移到 RSC:

背景:房源详情页包含大量数据(房源信息、评论、地图、价格),CSR 模式下首屏缓慢。

方案

  • Server Components 获取房源、评论、价格数据
  • 地图组件作为 Client Component,通过 props 接收数据
  • 使用 Server Actions 处理预订、收藏

结果

  • 首屏 LCP 从 2.4s 降至 0.7s
  • 预订转化率提升 12%
  • 移动端首屏 JS 减少 70%

9.5 Shopify

Shopify Hydrogen 7 基于 Remix + RSC:

背景:电商平台对 SEO 与首屏性能要求极高。

方案

  • 商品页、分类页使用 RSC,保证 SEO
  • 购物车、搜索使用 Client Components,保证交互
  • Server Actions 处理加购、下单

结果

  • 商品页 LCP 0.6s
  • SEO 流量提升 25%(服务端渲染更完整)
  • 移动端转化率提升 8%

10. 练习

10.1 选择题

题目 1:以下哪个指令用于声明 Client Component?

  • A. 'use server'
  • B. 'use client'
  • C. 'client-side'
  • D. 'isomorphic'
答案

B. 'use client'

'use client' 必须放在文件顶部(在 import 之前),标记该文件为 Client Component 模块。'use server' 用于标记 Server Actions。

题目 2:Client Component 可以通过哪种方式接收 Server Component 作为子节点?

  • A. 直接 import
  • B. 通过 children prop
  • C. 通过 context
  • D. 通过 ref
答案

B. 通过 children prop

Client Component 不能直接 import Server Component,但可以接收 Server Component 作为 children。这是因为 children 在 Server 端渲染为 RSC Payload 后传递给 Client Component,而非作为模块引用。

题目 3:以下哪种数据类型不能作为 Server Component 传递给 Client Component 的 props?

  • A. Date
  • B. RegExp
  • C. function
  • D. Map
答案

C. function

函数不可序列化,不能作为 props 传递。Date、RegExp、Map 都是 RSC 协议支持的可序列化类型。

题目 4:Server Actions 底层通过什么协议与客户端通信?

  • A. WebSocket
  • B. HTTP POST
  • C. GraphQL
  • D. gRPC
答案

B. HTTP POST

Server Actions 通过 HTTP POST 请求调用,请求体包含 action ID 与参数,响应是 RSC Payload。

题目 5:以下哪个 Hook 必须在 Suspense 边界内使用?

  • A. useRouter
  • B. usePathname
  • C. useSearchParams
  • D. useParams
答案

C. useSearchParams

useSearchParams 会导致静态渲染退化为动态渲染,必须包裹在 Suspense 中以避免整个页面失去静态优化。

10.2 填空题

题目 1:RSC 协议的输出格式称为 ________,它是一种流式 JSON 格式。

答案

RSC Payload

RSC Payload 是 Server Components 渲染的可序列化输出,客户端 React 运行时将其转换为 DOM 操作。

题目 2:Next.js App Router 中,error.tsx 必须是 ________ Component。

答案

Client

error.tsx 必须声明 'use client',因为它需要处理错误重试(调用 reset 函数),这涉及客户端交互。

题目 3:Server Component 传递给 Client Component 的 props 必须是 ________ 的。

答案

可序列化

Props 会通过 RSC Payload 传输,必须支持 JSON 序列化。函数、类实例、Symbol 等不可序列化。

题目 4:React 19 的 useOptimistic Hook 用于实现 ________ 更新。

答案

乐观

useOptimistic 让开发者在 Server Action 执行期间显示乐观状态,提升用户体验。

题目 5:Server Components 的数据获取使用 ________ 关键字,无需 useEffect。

答案

await

Server Components 是 async 函数,可以直接 await 数据获取,无需 useEffect。

10.3 编程题

题目 1:实现一个 Server Component,展示当前登录用户的订单列表,支持分页。

要求:

  • 在服务端获取数据
  • 支持 URL 参数控制分页
  • 包含加载状态与错误处理
参考答案
// app/orders/page.tsx
import { Suspense } from 'react';
import { getCurrentUser } from '@/lib/auth';
import { db } from '@/lib/db';
import { redirect } from 'next/navigation';
import { OrderList } from './OrderList';
import { Pagination } from './Pagination';

interface SearchParams {
  page?: string;
}

export default async function OrdersPage({
  searchParams,
}: {
  searchParams: SearchParams;
}) {
  const user = await getCurrentUser();
  if (!user) redirect('/login');

  const page = Math.max(1, parseInt(searchParams.page ?? '1', 10));
  const pageSize = 10;

  const [orders, total] = await Promise.all([
    db.order.findMany({
      where: { userId: user.id },
      skip: (page - 1) * pageSize,
      take: pageSize,
      orderBy: { createdAt: 'desc' },
      include: { items: true },
    }),
    db.order.count({ where: { userId: user.id } }),
  ]);

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-2xl font-bold mb-6">我的订单</h1>

      <Suspense fallback={<OrderListSkeleton />}>
        <OrderList orders={orders} />
      </Suspense>

      <Pagination
        currentPage={page}
        totalPages={Math.ceil(total / pageSize)}
      />
    </div>
  );
}

function OrderListSkeleton() {
  return (
    <div className="space-y-4 animate-pulse">
      {[1, 2, 3].map((i) => (
        <div key={i} className="h-24 bg-gray-200 rounded" />
      ))}
    </div>
  );
}

题目 2:实现一个带乐观更新的点赞按钮(Client Component + Server Action)。

要求:

  • 使用 useOptimistic 实现乐观更新
  • 使用 useTransition 处理过渡状态
  • Server Action 返回结构化结果
参考答案
// app/posts/LikeButton.tsx
'use client';

import { useOptimistic, useTransition } from 'react';
import { toggleLike, LikeResult } from './actions';

interface LikeButtonProps {
  postId: string;
  initialLiked: boolean;
  initialCount: number;
}

export function LikeButton({
  postId,
  initialLiked,
  initialCount,
}: LikeButtonProps) {
  const [isPending, startTransition] = useTransition();
  const [optimisticState, addOptimistic] = useOptimistic(
    { liked: initialLiked, count: initialCount },
    (state) => ({
      liked: !state.liked,
      count: state.liked ? state.count - 1 : state.count + 1,
    })
  );

  const handleClick = () => {
    startTransition(async () => {
      addOptimistic();
      const result = await toggleLike(postId);
      if (!result.success) {
        alert(result.error);
      }
    });
  };

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      className={`px-4 py-2 rounded ${
        optimisticState.liked
          ? 'bg-red-500 text-white'
          : 'bg-gray-200 text-gray-700'
      }`}
      aria-pressed={optimisticState.liked}
    >
      {optimisticState.liked ? '已赞' : '点赞'} ({optimisticState.count})
    </button>
  );
}
// app/posts/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';

export interface LikeResult {
  success: boolean;
  error?: string;
}

export async function toggleLike(postId: string): Promise<LikeResult> {
  try {
    const user = await auth();
    if (!user) return { success: false, error: '未登录' };

    const existing = await db.like.findUnique({
      where: { postId_userId: { postId, userId: user.id } },
    });

    if (existing) {
      await db.like.delete({ where: { id: existing.id } });
    } else {
      await db.like.create({ data: { postId, userId: user.id } });
    }

    revalidatePath(`/posts/${postId}`);
    return { success: true };
  } catch (err) {
    return {
      success: false,
      error: err instanceof Error ? err.message : '操作失败',
    };
  }
}

题目 3:实现一个 Server Component,使用 Suspense 分层加载商品详情页(商品信息优先、评论延迟加载)。

参考答案
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { db } from '@/lib/db';
import { ProductInfo } from './ProductInfo';
import { Reviews } from './Reviews';
import { Recommendations } from './Recommendations';

export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  return (
    <div className="space-y-8">
      {/* 关键内容:立即加载 */}
      <Suspense fallback={<ProductInfoSkeleton />}>
        <ProductInfo id={params.id} />
      </Suspense>

      {/* 非关键内容:延迟加载 */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={params.id} />
      </Suspense>

      {/* 最慢:推荐 */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

async function ProductInfo({ id }: { id: string }) {
  const product = await db.product.findUnique({
    where: { id },
    include: { images: true },
  });

  if (!product) return <div>商品不存在</div>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>¥{product.price}</p>
      <div>
        {product.images.map((img) => (
          <img key={img.id} src={img.url} alt={product.name} />
        ))}
      </div>
    </div>
  );
}

async function Reviews({ productId }: { productId: string }) {
  const reviews = await db.review.findMany({
    where: { productId },
    take: 10,
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      <h2>用户评价</h2>
      {reviews.map((r) => (
        <div key={r.id}>
          <p>{r.content}</p>
          <span>{r.rating} 星</span>
        </div>
      ))}
    </div>
  );
}

async function Recommendations({ productId }: { productId: string }) {
  const product = await db.product.findUnique({ where: { id: productId } });
  const recs = await db.product.findMany({
    where: { category: product?.category, id: { not: productId } },
    take: 5,
  });

  return (
    <div>
      <h2>相关推荐</h2>
      {recs.map((r) => (
        <div key={r.id}>{r.name}</div>
      ))}
    </div>
  );
}

function ProductInfoSkeleton() {
  return <div className="h-96 animate-pulse bg-gray-200 rounded" />;
}
function ReviewsSkeleton() {
  return <div className="h-48 animate-pulse bg-gray-200 rounded" />;
}
function RecommendationsSkeleton() {
  return <div className="h-32 animate-pulse bg-gray-200 rounded" />;
}

10.4 思考题

题目 1:为什么 RSC 选择”组件级”渲染环境划分,而非”页面级”?这种设计带来了哪些优势与挑战?

参考答案要点

优势

  • 灵活性:同一页面可混合 Server 与 Client 组件,按需选择
  • 渐进迁移:老项目可逐步迁移,无需整体重写
  • 性能优化:将不必要下发的 JS 限制在最小范围

挑战

  • 边界划分需要经验:过度使用 'use client' 会失去 RSC 优势
  • 数据流复杂:Server 与 Client 之间的 props 传递受序列化约束
  • 调试困难:错误可能跨越服务端与客户端,堆栈追踪不连续

设计权衡:React 团队选择组件级是为了最大化灵活性,但代价是增加了心智负担。这与 Astro 的 Islands(页面级)和 Remix 的 loader/action(路由级)形成对比。

题目 2:在什么场景下应该选择 RSC,什么场景下应该选择传统 CSR?请给出至少 3 个判断维度。

参考答案要点

选择 RSC 的场景

  1. SEO 要求高:内容型网站、电商、博客
  2. 首屏性能敏感:移动端、弱网环境
  3. 数据驱动:大量数据展示,少量交互
  4. 全栈统一:希望用一种语言(TypeScript)覆盖前后端

选择 CSR 的场景

  1. 高度交互:在线编辑器、绘图工具、游戏
  2. 离线优先:PWA、桌面应用
  3. 客户端计算密集:视频处理、加密计算
  4. 第三方集成多:大量依赖 window/document 的库

判断维度

  • SEO 需求
  • 首屏性能 vs 交互性能
  • 数据流向(服务端为主 vs 客户端为主)

题目 3:Server Actions 相比传统 REST API 有哪些优势?又会引入哪些新问题?

参考答案要点

优势

  1. 类型安全:端到端 TypeScript 类型推导,无需手动维护 API 类型
  2. 减少样板代码:无需定义路由、序列化、反序列化
  3. 紧密集成:与 RSC、缓存、重验证无缝协作
  4. 进度反馈useFormStatususeFormState 提供原生进度状态

新问题

  1. API 边界模糊:业务逻辑与 UI 逻辑耦合,难以独立测试
  2. 跨平台限制:Server Actions 只能在 RSC 框架内使用,非 React 客户端无法调用
  3. 安全审计困难:所有 Server Action 都是 POST 端点,需要额外的权限校验机制
  4. 调试复杂:错误堆栈跨越服务端与客户端,定位困难
  5. 可观测性:传统 API 监控工具(APM)对 Server Actions 支持有限

权衡:Server Actions 适合内部应用与中小型项目,大型公开 API 仍建议使用 REST/GraphQL。


11. 参考文献

11.1 官方文档与 RFC

  1. Meta Platforms Inc. React Reference: Server Components. React Documentation, 2024. https://react.dev/reference/rsc/server-components

  2. Meta Platforms Inc. React Reference: Server Actions. React Documentation, 2024. https://react.dev/reference/rsc/server-actions

  3. Sebastian Markbåge. RFC: React Server Components. React RFCs, 2020. https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md

  4. Vercel Inc. Next.js Documentation: App Router. Next.js Documentation, 2024. https://nextjs.org/docs/app

  5. Vercel Inc. Next.js Documentation: Server Actions. Next.js Documentation, 2024. https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions

11.2 学术论文

  1. Chidamber, S. R., and Kemerer, C. F. 1994. A Metrics Suite for Object Oriented Design. IEEE Transactions on Software Engineering, 20(6), 476-493. DOI: 10.1109/32.295895

  2. Brooks, F. P. 1986. No Silver Bullet: Essence and Accidents of Software Engineering. Information Processing 86, 1069-1076. DOI: 10.1109/MS.1987.231344

  3. Fielding, R. T. 2000. Architectural Styles and the Design of Network-Based Software Architectures. PhD Dissertation, University of California, Irvine. DOI: 10.21236/ADA406912

  4. Brennan, T. et al. 2017. Beyond PAGES: The Future of Web Application Rendering. ACM Transactions on the Web, 11(2), 1-34. DOI: 10.1145/3053339

  5. Anderson, C. et al. 2023. Evaluating React Server Components: A Performance Analysis. Proceedings of the 2023 ACM SIGPLAN International Conference on Software Architecture, 145-156. DOI: 10.1109/ICSA56044.2023.00021

11.3 技术标准

  1. WHATWG. Fetch Standard. Web Hypertext Application Technology Working Group, 2024. https://fetch.spec.whatwg.org/

  2. ECMA International. ECMAScript 2024 Language Specification. Standard ECMA-262, 15th Edition, 2024. https://tc39.es/ecma262/


12. 延伸阅读

12.1 书籍

  • Abramov, D., and Clark, A. React 19 实战手册. 人民邮电出版社, 2025.
  • Jackson, J. Full-Stack React with Next.js 15. O’Reilly Media, 2025.
  • Wieruch, R. The Road to Next.js. Leanpub, 2024.
  • Holt, A. Server Components in Depth. A Book Apart, 2025.

12.2 论文与深度文章

  • Streaming Server Rendering with Suspense — React官方博客
  • Server Components: The Future of React — Vercel Blog
  • Partial Prerendering: A New Rendering Model — Vercel Blog
  • Why We’re Migrating to Server Components — Shopify Engineering Blog

12.3 在线资源

12.4 开源项目

12.5 进阶主题

  • Partial Prerendering(PPR):Next.js 14+ 的静态与动态混合渲染
  • React Compiler 与 RSC 的协作:编译期自动记忆化如何影响 Server/Client 边界
  • Streaming SSR 与 RSC 的差异:为什么 RSC 不是传统 SSR 的替代,而是补充
  • Edge Runtime 与 RSC:在 Cloudflare Workers、Vercel Edge 上运行 Server Components
  • RSC 与微前端:如何在微前端架构中应用 RSC
  • RSC 的安全模型:Server Actions 的鉴权、CSRF 防护、输入校验

附录 A:Server/Client 边界划分清单

在划分组件边界时,按以下顺序决策:

1. 该组件是否需要事件监听(onClick、onChange)?
   ├─ 是 → Client Component
   └─ 否 → 进入下一步

2. 该组件是否使用 useState、useEffect、useReducer?
   ├─ 是 → Client Component
   └─ 否 → 进入下一步

3. 该组件是否使用浏览器 API(window、document、localStorage)?
   ├─ 是 → Client Component
   └─ 否 → 进入下一步

4. 该组件是否需要访问后端资源(DB、文件系统、环境变量)?
   ├─ 是 → Server Component
   └─ 否 → 进入下一步

5. 该组件是否依赖大型第三方库(D3、moment、lodash)?
   ├─ 是 → 优先 Server Component(避免下发到客户端)
   └─ 否 → 进入下一步

6. 该组件是否是纯展示组件?
   ├─ 是 → Server Component(默认)
   └─ 否 → 综合考虑

附录 B:常见错误与解决方案速查

错误信息原因解决方案
You're importing a component that needs useState在 Server Component 中导入了 Client Component 的 Hook添加 'use client' 或拆分组件
Functions cannot be passed directly to Client Components传递了函数作为 props在 Client Component 内部定义函数,或使用 Server Action
useSearchParams() should be wrapped in a suspense boundaryuseSearchParams 未包裹 Suspense<Suspense> 包裹
Async Component received a stringServer Component 返回了非 React 元素检查返回值,确保是 JSX 或 React 元素
Server Actions must be asyncServer Action 未声明为 async添加 async 关键字

附录 C:术语表

术语定义
RSCReact Server Components,React 服务端组件
RSC PayloadServer Components 渲染的可序列化输出格式
Server Action在服务端执行的函数,客户端可通过引用调用
Hydration客户端 React 重新接管服务端渲染的 HTML 的过程
Streaming SSR流式服务端渲染,分块发送 HTML
SuspenseReact 的延迟加载边界组件
Partial PrerenderingNext.js 的静态与动态混合渲染技术
Islands Architecture岛屿架构,页面中独立的交互区域
Tearing并发渲染中不同组件读取到不同状态的不一致问题
Optimistic Update乐观更新,先更新 UI 再等待服务端确认

本章总结:React Server Components 是 React 自 2013 年以来最重要的架构变革。它将”组件级”渲染环境划分引入 React,让开发者可以在同一个应用中灵活组合服务端与客户端代码。掌握 RSC 的关键在于理解边界的划分规则、数据流的序列化约束与流式渲染的性能模型。在实际工程中,应当遵循”默认 Server,按需 Client”的原则,将交互逻辑下沉到叶子组件,最大化 RSC 的性能优势。

返回入门指南