React Server Components 实战:从踩坑到落地
发布日期: 2026/07/23 阅读总量: 1

一、真实场景:一个列表页让我崩溃

2024年3月,我接手了一个电商后台管理系统。其中一个「订单列表页」需要展示 5000 条订单,每条订单要查询用户信息、商品详情、物流状态。传统 React 组件(Client Components)下,页面加载耗时 8.2 秒,白屏时间 3.5 秒。用户反馈「点一下等半天」。我尝试了各种优化:虚拟列表、懒加载、代码分割,效果有限。直到我用了 React Server Components(RSC),首屏加载时间降到 1.1 秒,白屏时间 0.3 秒。

二、问题分析:Client Components 的瓶颈

传统 React 组件在客户端渲染,意味着:

  • 所有数据请求在浏览器端发起,每个请求有网络延迟(平均 200ms)
  • JavaScript bundle 体积大(该页面 2.3MB),解析执行阻塞渲染
  • 数据库查询、API 调用在客户端无法直接执行,必须通过 API 层中转

压测数据(Chrome DevTools, Fast 3G 模拟):

指标Client ComponentsServer Components
首屏加载时间8.2s1.1s
白屏时间3.5s0.3s
JS Bundle 大小2.3MB0.8MB
API 请求次数153

三、方案对比:Server Components vs Client Components

3.1 核心区别

  • Server Components:在服务端渲染,不发送 JavaScript 到客户端,可直接访问数据库、文件系统、后端 API。组件代码不会出现在客户端 bundle 中。
  • Client Components:在客户端渲染,需要下载并执行 JavaScript,支持交互(useState, useEffect, onClick 等)。

3.2 适用场景

场景推荐组件类型原因
数据获取(列表、详情)Server减少客户端请求,直接服务端查询
静态内容展示Server无需交互,减少 JS 体积
表单、按钮、动画Client需要事件处理、状态管理
第三方库(如 Chart.js)Client依赖浏览器 API

四、完整代码实现

4.1 项目初始化(Next.js 14.2.0 + React 18.3.1)

npx create-next-app@14.2.0 rsc-demo --typescript --tailwind --eslint
cd rsc-demo
npm install prisma @prisma/client  # 数据库 ORM
npx prisma init

4.2 数据库 Schema(Prisma + SQLite)

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model Order {
  id        Int      @id @default(autoincrement())
  userId    Int
  productId Int
  status    String   @default("pending")
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])
  product   Product  @relation(fields: [productId], references: [id])
}

model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String
  orders Order[]
}

model Product {
  id    Int    @id @default(autoincrement())
  name  String
  price Float
  orders Order[]
}

4.3 种子数据(5000 条订单)

// prisma/seed.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

async function main() {
  // 创建 100 个用户
  const users = [];
  for (let i = 0; i < 100; i++) {
    users.push({ name: `User${i}`, email: `user${i}@example.com` });
  }
  await prisma.user.createMany({ data: users });

  // 创建 50 个商品
  const products = [];
  for (let i = 0; i < 50; i++) {
    products.push({ name: `Product${i}`, price: Math.random() * 1000 });
  }
  await prisma.product.createMany({ data: products });

  // 创建 5000 条订单
  const orders = [];
  for (let i = 0; i < 5000; i++) {
    orders.push({
      userId: Math.floor(Math.random() * 100) + 1,
      productId: Math.floor(Math.random() * 50) + 1,
      status: ['pending', 'shipped', 'delivered'][Math.floor(Math.random() * 3)],
    });
  }
  await prisma.order.createMany({ data: orders });
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

4.4 Server Components 实现(关键代码)

// app/orders/page.tsx (Server Component)
import { prisma } from '@/lib/prisma';

// 这个组件是 Server Component,默认在服务端渲染
export default async function OrdersPage() {
  // 直接查询数据库,无需 API 层
  const orders = await prisma.order.findMany({
    include: {
      user: true,
      product: true,
    },
    take: 5000,
  });

  return (
    

订单列表(共 {orders.length} 条)

    {orders.map((order) => (
  • 订单 #{order.id} 用户:{order.user.name} 商品:{order.product.name} 状态:{order.status} 时间:{order.createdAt.toLocaleDateString()}
  • ))}
); }

4.5 Client Components 实现(对比用)

// app/orders-client/page.tsx (Client Component)
'use client'; // 标记为 Client Component

import { useEffect, useState } from 'react';

export default function OrdersClientPage() {
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 必须通过 API 获取数据
    fetch('/api/orders')
      .then((res) => res.json())
      .then((data) => {
        setOrders(data);
        setLoading(false);
      });
  }, []);

  if (loading) return 
加载中...
; return (

订单列表(客户端渲染)

    {orders.map((order) => (
  • 订单 #{order.id} 用户:{order.user.name} 商品:{order.product.name} 状态:{order.status}
  • ))}
); }

4.6 混合使用:Server + Client 组合

// app/orders-mixed/page.tsx
import { prisma } from '@/lib/prisma';
import OrderActions from './OrderActions'; // Client Component

export default async function OrdersMixedPage() {
  const orders = await prisma.order.findMany({
    include: { user: true, product: true },
    take: 5000,
  });

  return (
    

订单管理

{orders.map((order) => (
{order.user.name} - {order.product.name} {/* 交互部分用 Client Component */}
))}
); } // app/orders-mixed/OrderActions.tsx 'use client'; import { useState } from 'react'; export default function OrderActions({ orderId, initialStatus }) { const [status, setStatus] = useState(initialStatus); const updateStatus = async (newStatus) => { await fetch('/api/orders/update', { method: 'POST', body: JSON.stringify({ orderId, status: newStatus }), }); setStatus(newStatus); }; return ( ); }

五、效果数据

测试环境:MacBook Pro M1, Node.js 20.11.0, Next.js 14.2.0, SQLite, Chrome 124, Fast 3G 模拟。

指标Client ComponentsServer Components混合模式
首屏加载时间8.2s1.1s1.3s
白屏时间3.5s0.3s0.4s
JS Bundle 大小2.3MB0.8MB1.1MB
API 请求次数1535
TTI (可交互时间)9.5s1.5s2.0s
Lighthouse 性能评分429691

六、避坑指南(我踩过的 5 个坑)

坑 1:Server Components 里用了 useState

错误代码:

// 这是 Server Component,不能用 useState
export default function BadComponent() {
  const [count, setCount] = useState(0); // ❌ 报错
  return 
{count}
; }

解决方案:把需要状态的逻辑抽到 Client Component 中,Server Component 只负责数据获取和静态渲染。

坑 2:Server Components 里直接用了浏览器 API

错误代码:

export default function BadComponent() {
  const width = window.innerWidth; // ❌ 服务端没有 window
  return 
{width}
; }

解决方案:使用 Client Component 或通过 useEffect 在客户端获取。

坑 3:Server Components 和 Client Components 混用时的 props 传递

Server Component 不能直接传递函数给 Client Component:

// Server Component
export default function Parent() {
  const handleClick = () => { console.log('click'); }; // ❌ 不能传递
  return ;
}

解决方案:Client Component 内部定义事件处理函数,或通过 Server Actions(Next.js 14+)传递。

坑 4:数据缓存问题

Server Components 默认会缓存数据,导致更新后页面不刷新。解决方案:使用 revalidatePathrevalidateTag 强制刷新。

// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function updateOrder(formData) {
  // 更新数据库...
  revalidatePath('/orders'); // 刷新订单页面
}

坑 5:Server Components 中直接使用第三方库(如 Chart.js)

Chart.js 依赖 DOM 和浏览器 API,在 Server Component 中会报错。解决方案:动态导入并标记为 Client Component。

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('./Chart'), { ssr: false }); // 禁用 SSR

七、总结

React Server Components 不是银弹,但适合数据密集型、交互少的页面。核心原则:Server 负责数据,Client 负责交互。记住:能用 Server 的尽量用 Server,能减少的 JS 一定要减少。