返回首页
🎨 前端 / Web

React 19 + Server Components 实战:完整迁移指南

React Server Components 在 2026 年已经成熟。本文用真实电商项目演示从 Pages Router 到 App Router + RSC 的完整迁移。

React · RSC · Next.js · 前端 · 性能
📰

今日技术简讯

📰 技术简讯 · 2026-06-09

今日聚合 7 条热门技术内容。

🤖 AI / LLM

1. Claude 4.1 推出 Computer Use GA

2. Mistral 发布 Codestral 25B

🎨 前端 / Web

3. React 19 + RSC 实战经验

4. TanStack Query 5 进入 RC

⚙️ 后端 / 架构

5. Postgres 进入 ML 时代:pgai 0.5

🚀 独立开发 / OPC

6. 《Indie Founder Playbook》第三版发布

7. Resend 推出 Marketing API


数据来源:HN / Reddit / 各厂博客 采集时间:2026-06-09 09:00 (UTC+8)

📝

今日深度文

React 19 + Server Components 实战:完整迁移指南

一句话结论:RSC 不是"另一种 SSR",而是"前端架构的重新定义"。迁移成本不低,但收益巨大。

背景

React Server Components(RSC)从 2023 年提出,到 2026 年已经成熟:

  • Next.js 16 完全基于 RSC(默认所有组件是 Server Component)
  • Vite + React 19 支持 RSC
  • 各大厂生产环境使用:Vercel / Netflix / Notion / GitHub

但 RSC 是一个架构变革,不是简单的 API 变化。下面是真实迁移经验。

RSC 的核心概念

传统 React 组件(Client Component)

// app/products/page.tsx - 传统写法
'use client';

import { useState, useEffect } from 'react';

export default function Products() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts);
  }, []);
  
  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

问题

  1. JS bundle 包含所有代码(即使只是展示)
  2. 用户看到 loading → 再看到内容(瀑布流)
  3. SEO 不友好(虽然有 Next.js 兜底)

Server Component

// app/products/page.tsx - RSC 写法
import { getProducts } from '@/lib/db';

export default async function Products() {
  // 在服务器上直接查数据库
  const products = await getProducts();
  
  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

优势

  1. ✅ 零 JS bundle(只在服务器跑一次)
  2. ✅ 首屏直达数据(无 loading)
  3. ✅ 完美 SEO
  4. ✅ 数据库访问不需要 API endpoint

Client Component(必须用 'use client')

// components/AddToCart.tsx
'use client';

import { useState } from 'react';

export function AddToCart({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);
  
  return (
    <button onClick={async () => {
      setLoading(true);
      await fetch('/api/cart', { 
        method: 'POST', 
        body: JSON.stringify({ productId }) 
      });
      setLoading(false);
    }}>
      {loading ? '加入中...' : '加入购物车'}
    </button>
  );
}

核心规则

  • 默认是 Server Component(无 JS)
  • 需要交互 → 加 'use client'
  • Server Component 可以嵌套 Client Component
  • Client Component 不能 import Server Component(边界问题)

真实案例:电商首页迁移

迁移前(Pages Router)

// pages/index.tsx
export async function getServerSideProps() {
  const products = await fetchProducts();
  const recommendations = await fetchRecommendations(user);
  return { props: { products, recommendations } };
}

export default function Home({ products, recommendations }) {
  return (
    <div>
      <Hero />
      <ProductList products={products} />
      <Recommendations items={recommendations} />
      <Newsletter />
    </div>
  );
}

指标

  • JS bundle:420KB
  • 首屏 LCP:2.8s
  • SEO 评分:78

迁移后(App Router + RSC)

// app/page.tsx - 顶层 Server Component
import { getProducts } from '@/lib/db';
import { Hero } from '@/components/Hero';
import { ProductList } from '@/components/ProductList';
import { Recommendations } from '@/components/Recommendations';
import { Newsletter } from '@/components/Newsletter';

export default async function Home() {
  // 并行数据获取
  const [products, recommendations] = await Promise.all([
    getProducts(),
    getRecommendations(),
  ]);
  
  return (
    <div>
      <Hero />
      <ProductList products={products} />
      <Recommendations items={recommendations} />
      <Newsletter />
    </div>
  );
}

// 优化:用 Suspense 流式渲染
import { Suspense } from 'react';

export default function Home() {
  return (
    <div>
      <Hero />
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>
      <Newsletter />
    </div>
  );
}

指标

  • JS bundle:180KB(-57%)
  • 首屏 LCP:1.2s(-57%)
  • SEO 评分:96

5 个关键模式

模式 1:Server Component → Client Component 边界

// ✅ 正确:Server 包 Client
// app/dashboard/page.tsx (Server)
import { Chart } from './Chart'; // Client

export default async function Dashboard() {
  const data = await getData();
  return <Chart data={data} />;
}

// ❌ 错误:Client 试图 import Server
// components/Chart.tsx (Client)
import { ServerOnlyChart } from './ServerOnlyChart'; // 报错

技巧:把 Server Component 作为 children 传给 Client Component:

// components/Modal.tsx (Client)
'use client';

export function Modal({ children, isOpen }: { 
  children: React.ReactNode;
  isOpen: boolean;
}) {
  return isOpen ? <div className="modal">{children}</div> : null;
}

// app/dashboard/page.tsx (Server)
export default async function Page() {
  const data = await getData();
  return (
    <Modal isOpen={true}>
      <ServerChart data={data} />  {/* Server 在 Modal 里渲染 */}
    </Modal>
  );
}

模式 2:Streaming SSR

// app/products/page.tsx
import { Suspense } from 'react';

export default function ProductsPage() {
  return (
    <div>
      <h1>产品列表</h1>
      <Suspense fallback={<Skeleton />}>
        <ProductList />  {/* Server Component 异步加载 */}
      </Suspense>
    </div>
  );
}

async function ProductList() {
  const products = await getProducts(); // 即使慢,也能先渲染其他部分
  return products.map(p => <ProductCard key={p.id} product={p} />);
}

效果:用户立即看到骨架屏 + 标题,ProductList 加载好后自动填充。

模式 3:Server Actions(替代 API routes)

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

import { revalidatePath } from 'next/cache';

export async function addProduct(formData: FormData) {
  const product = {
    name: formData.get('name'),
    price: Number(formData.get('price')),
  };
  
  await db.products.insert(product);
  revalidatePath('/products'); // 重新渲染
}
// components/AddProductForm.tsx
'use client';

import { addProduct } from '@/app/actions';

export function AddProductForm() {
  return (
    <form action={addProduct}>
      <input name="name" />
      <input name="price" type="number" />
      <button type="submit">添加</button>
    </form>
  );
}

优势:不用写 API endpoint,不用 fetch,不用管错误处理。

模式 4:共享组件的 Server / Client 双版本

// components/Button.tsx - 双版本
// Server 版本(无 JS)
export function Button({ children, ...props }: ButtonProps) {
  return <button {...props}>{children}</button>;
}

// Client 版本(有 JS)
'use client';
export function ClientButton({ children, onClick, ...props }: ButtonProps) {
  return <button onClick={onClick} {...props}>{children}</button>;
}

// 使用时按需导入
import { Button } from '@/components/Button'; // Server 版本(默认)
import { ClientButton } from '@/components/Button'; // Client 版本

模式 5:数据获取的 Suspense 边界

// app/dashboard/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <Sidebar />
      <main>
        <Suspense fallback={<PageSkeleton />}>
          {children}
        </Suspense>
      </main>
    </div>
  );
}

// 任意子页面都可以 await,不必整体阻塞

性能优化清单

- [ ] 把所有数据获取移到 Server Component
- [ ] 用 Promise.all 并行获取
- [ ] 用 Suspense 流式渲染慢的部分
- [ ] 删除不必要的 'use client'
- [ ] 用 Server Actions 替代 API routes
- [ ] 用 next/dynamic 懒加载 Client Component
- [ ] 用 next/image 优化图片
- [ ] 用 React.cache() 优化重复请求

5 个常见坑

坑 1:Client Component 里 await 数据

'use client';

export default function Page() {
  const data = await fetchData(); // ❌ Client Component 不能 await
}

坑 2:Server Component 里用 useState

export default async function Page() {
  const [count, setCount] = useState(0); // ❌ Server 没有 hooks
}

坑 3:Client Component 嵌套 Server Component

'use client';
import { ServerComponent } from './Server'; // ❌ Client 不能 import Server

坑 4:忘记给 Server Action 加 'use server'

// app/actions.ts
export async function deleteProduct(id: string) {  // ❌ 缺 'use server'
  await db.products.delete(id);
}

坑 5:滥用 'use client'

很多团队"安全起见"全加 'use client',结果 bundle 反而更大。默认不加,需要交互才加

我的看法

RSC 是前端架构的重要演进

  1. 减少 JS bundle:性能提升立竿见影
  2. 简化数据流:不用 manage client state 缓存
  3. 更好的 SEO:服务端渲染变成默认

但迁移成本不低:

  • 需要团队重新学习"边界"概念
  • 需要重构现有 Client Component
  • 需要重新考虑状态管理(部分状态可以放服务端)

我的建议

  • 新项目:直接用 App Router + RSC
  • 旧项目:渐进式迁移,先把"只读"的页面转 RSC
  • 状态复杂的页面:保留 Client Component,不必强转

参考


本文迁移示例基于真实电商项目,2026 年 Q2 实测数据。

📚 同主题文章

🎨 前端 / Web 分类更多