next 精选推荐

React Server Components 原理精讲:从概念到实现的深度剖析

HTMLPAGE 团队
20 分钟阅读

深入解析 React Server Components 的设计理念、工作原理、渲染流程,理解 RSC 如何改变前端开发范式,掌握 Server/Client 组件的边界划分与最佳实践。

#React Server Components #RSC #React #Next.js #服务端渲染

React Server Components 原理精讲

引言:RSC 是什么,不是什么

React Server Components(RSC)可能是 React 近年来最重要的架构演进。但围绕它的误解也最多:有人以为它就是 SSR 的新名字,有人以为它会取代客户端 React,还有人以为它只是 Next.js 的专属功能。

这篇文章的目标是帮你建立对 RSC 的准确理解——从设计动机到工作原理,从使用方式到最佳实践。

第一部分:为什么需要 RSC

1.1 传统 React 应用的痛点

让我们从一个典型的 React 应用说起:

// 传统客户端 React 应用
function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    Promise.all([
      fetch(`/api/products/${productId}`),
      fetch(`/api/products/${productId}/reviews`)
    ])
      .then(([p, r]) => Promise.all([p.json(), r.json()]))
      .then(([product, reviews]) => {
        setProduct(product);
        setReviews(reviews);
        setLoading(false);
      });
  }, [productId]);

  if (loading) return <Skeleton />;

  return (
    <div>
      <ProductDetail product={product} />
      <ReviewList reviews={reviews} />
    </div>
  );
}

这个模式存在几个根本性问题:

┌──────────────────────────────────────────────────────────────┐
│                   传统 SPA 的问题                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 客户端数据瀑布流                                          │
│     浏览器 → 下载 JS → 执行 → 发起请求 → 等待响应 → 渲染     │
│     用户看到内容的时间 = 所有步骤的总和                       │
│                                                              │
│  2. Bundle 膨胀                                               │
│     - React + ReactDOM: ~40KB                                │
│     - 组件代码: 可能几百 KB                                   │
│     - 依赖库: 可能几 MB                                       │
│     所有这些都要下载到客户端                                   │
│                                                              │
│  3. 数据获取与组件分离                                        │
│     - 数据获取在 useEffect 中                                 │
│     - 容易产生 N+1 问题                                       │
│     - 难以优化和预取                                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.2 SSR 解决了什么,没解决什么

传统 SSR 解决了首屏渲染问题,但引入了新问题:

// 传统 SSR (如 Next.js Pages Router)
export async function getServerSideProps() {
  const product = await fetchProduct(id);
  const reviews = await fetchReviews(id);
  return { props: { product, reviews } };
}

function ProductPage({ product, reviews }) {
  return (
    <div>
      <ProductDetail product={product} />
      <ReviewList reviews={reviews} />
    </div>
  );
}

SSR 的问题:

┌──────────────────────────────────────────────────────────────┐
│                    传统 SSR 的问题                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 双重渲染                                                  │
│     - 服务端渲染一次生成 HTML                                 │
│     - 客户端 hydration 又渲染一次                             │
│     - 同样的组件代码执行两遍                                   │
│                                                              │
│  2. 全量 Hydration                                           │
│     - 即使是静态内容也需要 hydrate                            │
│     - 客户端 JS 依然很大                                      │
│     - TTI (可交互时间) 受影响                                 │
│                                                              │
│  3. 数据获取仍集中在页面级                                    │
│     - getServerSideProps 只在页面组件中                       │
│     - 子组件无法独立获取数据                                   │
│     - Props drilling 问题依然存在                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.3 RSC 的设计目标

RSC 试图从根本上解决这些问题:

┌──────────────────────────────────────────────────────────────┐
│                    RSC 的核心理念                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 组件级别的渲染位置选择                                    │
│     - 每个组件可以独立决定在服务端还是客户端渲染              │
│     - 而不是整个页面统一决定                                   │
│                                                              │
│  2. 零客户端 JS 的服务端组件                                  │
│     - Server Components 的代码不会发送到客户端                │
│     - 只发送渲染结果                                          │
│                                                              │
│  3. 数据获取与组件共置                                        │
│     - 组件可以直接 await 数据                                 │
│     - 无需 useEffect 或状态管理                               │
│     - 数据获取自动并行                                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

第二部分:RSC 工作原理

2.1 两种组件类型

RSC 引入了两种组件类型:

// Server Component (默认)
// - 只在服务端执行
// - 可以 async/await
// - 可以直接访问数据库、文件系统等
// - 不能使用 hooks (useState, useEffect 等)
// - 不能使用浏览器 API

async function ProductDetail({ id }) {
  // 直接在组件中获取数据
  const product = await db.products.findUnique({ where: { id } });
  
  // 可以使用服务端资源
  const description = await fs.readFile(`./data/${id}.md`);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <Markdown content={description} />
    </div>
  );
}

// Client Component
// - 使用 'use client' 指令标记
// - 在服务端和客户端都执行
// - 可以使用所有 React 特性
// - 可以使用浏览器 API

'use client'

function AddToCartButton({ productId }) {
  const [pending, setPending] = useState(false);
  
  async function handleClick() {
    setPending(true);
    await addToCart(productId);
    setPending(false);
  }
  
  return (
    <button onClick={handleClick} disabled={pending}>
      {pending ? '添加中...' : '加入购物车'}
    </button>
  );
}

2.2 组件树与边界

// 组件树中的 Server/Client 边界

// ✅ 有效的组件树
<ServerComponent>           // Server
  <AnotherServer>           // Server
    <ClientComponent>       // Client - 这是边界
      <ChildComponent />    // 自动成为 Client
    </ClientComponent>
  </AnotherServer>
</ServerComponent>

// Server Component 可以导入 Client Component
import { ClientButton } from './ClientButton'

async function ServerPage() {
  const data = await fetchData();
  return (
    <div>
      <h1>{data.title}</h1>
      <ClientButton />  {/* Client 组件作为子组件 */}
    </div>
  );
}

// ❌ 错误:Client Component 不能导入 Server Component
'use client'
import { ServerWidget } from './ServerWidget'  // 错误!

function ClientPage() {
  return <ServerWidget />;  // 不允许
}

// ✅ 正确:通过 children 传递
'use client'

function ClientWrapper({ children }) {
  const [show, setShow] = useState(true);
  return show ? children : null;  // children 可以是 Server Component
}

// 使用
<ClientWrapper>
  <ServerWidget />  {/* 通过 props 传入,可以工作 */}
</ClientWrapper>

2.3 RSC Payload:渲染结果的传输格式

RSC 的核心创新之一是引入了新的传输格式:

// RSC Payload 示例
// 这不是 HTML,而是一种特殊的序列化格式

/*
0:["$","div",null,{"children":[
  ["$","h1",null,{"children":"商品详情"}],
  ["$","$L1",null,{"productId":"123"}],
  ["$","p",null,{"children":"这是一段描述..."}]
]}]
1:I["./ClientButton.js",["ClientButton"]]
*/

// 解读:
// - 0: 是 Server Component 的渲染结果
// - $L1: 表示一个 Client Component 的占位符
// - 1:I: 是 Client Component 的模块引用

// 这种格式的优势:
// 1. 可流式传输(不需要等待全部完成)
// 2. 可增量更新(只更新变化的部分)
// 3. 保持组件树结构(便于 reconciliation)

2.4 渲染流程详解

┌──────────────────────────────────────────────────────────────┐
│                    RSC 渲染流程                                │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 服务端渲染阶段                                            │
│     ┌─────────────┐                                          │
│     │ 请求到达    │                                          │
│     └──────┬──────┘                                          │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ 渲染 Server 组件树  │                                  │
│     │ (执行 async 函数)   │                                  │
│     └──────┬──────────────┘                                  │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ 遇到 Client 组件    │                                  │
│     │ 生成占位符和引用    │                                  │
│     └──────┬──────────────┘                                  │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ 生成 RSC Payload    │                                  │
│     │ (可流式传输)        │                                  │
│     └──────┬──────────────┘                                  │
│                                                              │
│  2. 客户端处理阶段                                            │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ 接收 RSC Payload    │                                  │
│     └──────┬──────────────┘                                  │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ 重建虚拟 DOM 树     │                                  │
│     │ 加载 Client 组件    │                                  │
│     └──────┬──────────────┘                                  │
│            ↓                                                 │
│     ┌─────────────────────┐                                  │
│     │ Hydrate Client 组件 │                                  │
│     │ (选择性 Hydration)  │                                  │
│     └─────────────────────┘                                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

第三部分:实战中的边界划分

3.1 决定组件类型的原则

// 问:这个组件应该是 Server 还是 Client?

// 判断标准 1:是否需要浏览器 API?
function ComponentA() {
  // 使用 localStorage - 需要 'use client'
  const theme = localStorage.getItem('theme');
  return <div className={theme}>...</div>;
}

// 判断标准 2:是否需要事件处理?
function ComponentB() {
  // onClick 需要 'use client'
  return <button onClick={() => alert('clicked')}>Click</button>;
}

// 判断标准 3:是否需要 React Hooks?
function ComponentC() {
  // useState 需要 'use client'
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

// 判断标准 4:是否只是展示数据?
async function ComponentD({ id }) {
  // 纯展示 - 保持 Server Component
  const data = await fetchData(id);
  return <div>{data.title}</div>;
}

3.2 常见模式

模式 1:提升 Client 边界

// ❌ 整个组件变成 Client
'use client'
function ProductCard({ product }) {
  return (
    <div>
      <img src={product.image} alt={product.name} />
      <h2>{product.name}</h2>
      <p>{product.description}</p>  {/* 大段静态内容 */}
      <button onClick={() => addToCart(product.id)}>
        加入购物车
      </button>
    </div>
  );
}

// ✅ 只将交互部分变成 Client
function ProductCard({ product }) {
  return (
    <div>
      <img src={product.image} alt={product.name} />
      <h2>{product.name}</h2>
      <p>{product.description}</p>  {/* 保持在 Server */}
      <AddToCartButton productId={product.id} />  {/* 只有按钮是 Client */}
    </div>
  );
}

'use client'
function AddToCartButton({ productId }) {
  return (
    <button onClick={() => addToCart(productId)}>
      加入购物车
    </button>
  );
}

模式 2:组合而非继承

// ✅ 使用 children 传递 Server 组件

'use client'
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  return (
    <div className="modal">
      <button onClick={onClose}>关闭</button>
      {children}  {/* children 可以是 Server Component */}
    </div>
  );
}

// 使用
async function ProductPage() {
  const product = await fetchProduct(id);
  
  return (
    <Modal isOpen={true} onClose={handleClose}>
      {/* 这个组件在服务端渲染 */}
      <ProductDetail product={product} />
    </Modal>
  );
}

模式 3:Context Provider 模式

// Context Provider 必须是 Client Component
// 但这不意味着所有子组件都必须是 Client

// providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'

export function Providers({ children }) {
  return (
    <ThemeProvider>
      {children}  {/* children 可以是 Server Components */}
    </ThemeProvider>
  );
}

// layout.tsx (Server Component)
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>
          {children}  {/* 页面仍然可以是 Server Component */}
        </Providers>
      </body>
    </html>
  );
}

3.3 性能优化策略

// 策略 1:延迟加载 Client 组件
import dynamic from 'next/dynamic'

// 重型编辑器只在需要时加载
const HeavyEditor = dynamic(() => import('./HeavyEditor'), {
  loading: () => <p>加载编辑器...</p>,
  ssr: false  // 禁用 SSR,减少服务端负担
});

// 策略 2:使用 Suspense 流式加载
async function Dashboard() {
  return (
    <div>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />  {/* 异步加载 */}
      </Suspense>
      
      <Suspense fallback={<ChartSkeleton />}>
        <Chart />  {/* 独立的流 */}
      </Suspense>
    </div>
  );
}

// 策略 3:预加载数据
import { preload } from 'react-dom'

async function ProductPage({ id }) {
  // 预加载图片
  preload(`/images/products/${id}.jpg`, { as: 'image' });
  
  const product = await fetchProduct(id);
  return <ProductDetail product={product} />;
}

第四部分:数据流与状态管理

4.1 Server Components 中的数据获取

// 直接 async/await
async function UserProfile({ userId }) {
  const user = await db.users.findUnique({ where: { id: userId } });
  const posts = await db.posts.findMany({ where: { authorId: userId } });
  
  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

// 并行数据获取
async function Dashboard() {
  // 这些请求会并行执行
  const [stats, users, orders] = await Promise.all([
    fetchStats(),
    fetchUsers(),
    fetchOrders()
  ]);
  
  return (
    <div>
      <StatsCard stats={stats} />
      <UserTable users={users} />
      <OrderTable orders={orders} />
    </div>
  );
}

// 使用 fetch 缓存
async function ProductList() {
  // 相同的请求会自动去重
  const products = await fetch('/api/products', {
    next: { revalidate: 60 }  // 缓存 60 秒
  }).then(r => r.json());
  
  return products.map(p => <ProductCard key={p.id} product={p} />);
}

4.2 Server Actions:从客户端修改服务端数据

// actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function addToCart(productId: string) {
  // 这个函数在服务端执行
  await db.cart.create({
    data: { productId, userId: getCurrentUser().id }
  });
  
  // 重新验证购物车页面的缓存
  revalidatePath('/cart');
}

export async function updateProfile(formData: FormData) {
  const name = formData.get('name');
  const email = formData.get('email');
  
  await db.users.update({
    where: { id: getCurrentUser().id },
    data: { name, email }
  });
  
  revalidatePath('/profile');
}

// 在 Client Component 中使用
'use client'

function AddToCartButton({ productId }) {
  async function handleClick() {
    await addToCart(productId);
    // 页面会自动更新
  }
  
  return <button onClick={handleClick}>加入购物车</button>;
}

// 在表单中使用
function ProfileForm({ user }) {
  return (
    <form action={updateProfile}>
      <input name="name" defaultValue={user.name} />
      <input name="email" defaultValue={user.email} />
      <button type="submit">保存</button>
    </form>
  );
}

4.3 Client State vs Server State

// Client State:UI 状态、用户输入
'use client'

function SearchBox() {
  // 这是客户端状态 - 即时响应用户输入
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);
  
  useEffect(() => {
    if (query.length > 2) {
      fetchSuggestions(query).then(setSuggestions);
    }
  }, [query]);
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SuggestionList items={suggestions} />
    </div>
  );
}

// Server State:业务数据
async function ProductPage({ id }) {
  // 这是服务端状态 - 来自数据库
  const product = await fetchProduct(id);
  
  return (
    <div>
      <h1>{product.name}</h1>
      <SearchBox />  {/* 客户端状态组件 */}
    </div>
  );
}

// 混合场景:乐观更新
'use client'

function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [pending, setPending] = useState(false);
  
  async function handleLike() {
    // 乐观更新
    setLikes(prev => prev + 1);
    setPending(true);
    
    try {
      await likePost(postId);  // Server Action
    } catch {
      setLikes(prev => prev - 1);  // 回滚
    } finally {
      setPending(false);
    }
  }
  
  return (
    <button onClick={handleLike} disabled={pending}>
      ❤️ {likes}
    </button>
  );
}

第五部分:常见误区与最佳实践

5.1 误区澄清

// 误区 1:RSC 就是 SSR
// ❌ 错误理解
// RSC 和 SSR 是不同概念

// SSR:组件在服务端渲染成 HTML,然后客户端 hydrate
// RSC:Server Components 只在服务端运行,Client Components 正常 hydrate

// 误区 2:所有组件都应该是 Server Component
// ❌ 不现实
// 需要交互的组件必须是 Client Component

// 误区 3:'use client' 意味着只在客户端运行
// ❌ 部分正确
// Client Components 首次渲染时也在服务端执行(用于生成 HTML)

// 误区 4:Server Component 不能有子 Client Component
// ❌ 错误
// Server Component 可以导入和渲染 Client Component

// 误区 5:添加 'use client' 后整个文件都是客户端代码
// ✅ 正确
// 'use client' 是文件级别的声明

5.2 最佳实践总结

// ✅ 实践 1:默认使用 Server Components
// 只有需要时才标记 'use client'

// ✅ 实践 2:将 Client 边界推到叶子节点
// 最小化 Client Components 的范围

// ✅ 实践 3:使用 composition 模式
// 通过 children 传递 Server Components

// ✅ 实践 4:分离数据和交互
async function ProductCard({ id }) {
  const product = await fetchProduct(id);  // 数据获取在 Server
  return (
    <Card>
      <CardContent product={product} />  {/* 静态内容在 Server */}
      <CardActions productId={id} />      {/* 交互在 Client */}
    </Card>
  );
}

// ✅ 实践 5:使用 Suspense 处理异步
async function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <AsyncContent />
    </Suspense>
  );
}

// ✅ 实践 6:避免在渲染时传递函数给 Client Components
// ❌
<ClientComponent onAction={() => serverAction()} />

// ✅
<ClientComponent actionId="specific-action" />
// 然后在 ClientComponent 中导入并调用 serverAction

结语:范式转变的意义

RSC 不只是一个新功能,它代表了 React 架构的范式转变:从"所有代码都在客户端运行"到"代码在最适合的地方运行"。

这个转变的意义在于:

  • 更好的性能:减少客户端 JS,加快首屏
  • 更好的开发体验:数据获取变得简单直观
  • 更好的用户体验:流式渲染,更快的交互

学习 RSC 需要时间和实践,但理解其原理会让你在日常开发中做出更好的决策。


参考资源