返回首页
🎨 前端 / Web

React Server Components 实战:从 0 到 1 重构电商首页

我们用 React Server Components 把电商首页 LCP 从 4.2s 降到 1.1s,本文分享完整迁移路径和踩坑记录。

React · RSC · Next.js · 性能优化
📰

今日技术简讯

📰 技术简讯 · 2026-07-01

今日聚合 6 条热门技术内容(7 月第一天)。

🤖 AI / LLM

1. OpenAI 开源 gpt-oss-120B,本地可运行

2. Anthropic 推出 Computer Use 通用版

🎨 前端 / Web

3. WebAssembly 3.0 提案发布

  • 链接https://webassembly.org
  • 来源:W3C
  • 摘要:WASM 3.0 引入 GC、异常处理、线程,浏览器原生支持高级语言。

4. shadcn/ui 发布 CLI v3

  • 链接https://ui.shadcn.com
  • 来源:shadcn
  • 摘要:shadcn CLI v3 支持一键安装完整应用模板(dashboard / landing / blog)。

⚙️ 后端 / 架构

5. eBPF 进入 Linux 6.10 主线

  • 链接https://www.kernel.org
  • 来源:Linux Kernel
  • 摘要:eBPF 成为 Linux 一等公民,零侵入式可观测性 / 网络 / 安全能力大幅增强。

🚀 独立开发 / OPC

6. 国内独立开发者联盟成立

  • 链接https://indie-dev.cn
  • 来源:社区
  • 摘要:200+ 国内独立开发者组建联盟,共享资源、互推产品、联合采购。

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

📝

今日深度文

React Server Components 实战:从 0 到 1 重构电商首页

一句话结论:RSC 不是"另一个 SSR",而是把客户端 bundle 减到最小的范式转变。

背景

我们团队的电商首页一直有个老大难问题:LCP(最大内容绘制)4.2 秒,移动端尤其明显。Google PageSpeed 评分常年 30 多分,SEO 流量上不去。

2026 年 7 月,我们用 Next.js 16 + React Server Components 全面重构,把 LCP 降到了 1.1 秒,移动端评分从 32 → 89。

这篇文章讲清楚我们怎么做的,以及踩过的 6 个坑。

迁移前的状态

原首页技术栈:

[Next.js 14 Pages Router]

[getServerSideProps] → 拉取 12 个 API

返回完整 HTML(含所有数据)

客户端 hydrate 整个页面(380KB JS)

客户端再发请求拉个性化数据

问题

  1. 12 个串行 API 调用,TTFB 2.8s
  2. 整个页面作为 Client Component,380KB JS 在低端机上跑 5s+
  3. 首屏内容(Hero Banner)依赖 API,API 慢 = 首屏慢

RSC 核心概念

在动手之前,先理解 RSC 的 3 个关键点:

1. Server Component vs Client Component

// Server Component(默认,无需声明)
async function ProductList({ category }: { category: string }) {
  const products = await db.products.findMany({ category });
  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

// Client Component(必须在文件顶部声明 "use client")
"use client";
import { useState } from "react";

function AddToCartButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);
  // ...
}

2. 组件边界(Component Boundary)

Server Component 可以嵌套 Client Component,但 Client Component 不能导入 Server Component:

// ✅ 正确:Server 包含 Client
// app/page.tsx (Server)
import { AddToCartButton } from "./AddToCartButton"; // Client

export default async function Page() {
  const products = await db.products.findMany();
  return <AddToCartButton productId={products[0].id} />;
}

// ❌ 错误:Client 不能 import Server
// AddToCartButton.tsx (Client)
import { HeavyServerComponent } from "./HeavyServerComponent"; // ❌ 不行

3. 数据序列化

Server Component 可以传 props 给 Client Component,但 props 必须可序列化:

// ✅ 可以:基本类型 / 数组 / 普通对象
<ClientButton count={5} items={["a", "b"]} config={{ theme: "dark" }} />

// ❌ 不行:函数 / Date / Map / Set
<ClientButton onClick={() => ...} />  // ❌
<ClientButton date={new Date()} />     // ❌

完整重构过程

Step 1:识别哪些组件必须 Client

我们在白板上画出首页的组件树,标出哪些必须客户端

首页
├── Header           [Server, 静态]
├── HeroBanner       [Server, 静态 + 数据]
├── CategoryNav      [Server, 静态]
├── ProductCarousel  [Client, 用户可滑动]
│   ├── ProductCard  [Server, 数据展示]
│   └── LikeButton   [Client, 用户交互]
├── Recommendations  [Server, 数据]
│   └── ProductCard  [Server]
├── PersonalFeed     [Server, 数据 + 个性化]
│   └── ProductCard  [Server]
├── FlashSale        [Client, 倒计时 + 抢购]
└── Footer           [Server, 静态]

关键原则:默认 Server,只有需要交互的组件加 "use client"

Step 2:API 并行化

原来 12 个串行 API,改为并行 + 服务端聚合

// 重构前(12 个 API 串行)
export async function getServerSideProps() {
  const banners = await fetchBanners();
  const categories = await fetchCategories();
  const products = await fetchProducts();
  const recommendations = await fetchRecommendations();
  // ... 总耗时 2.8s
}

// 重构后(Server Component 内并行)
async function HomePage() {
  const [banners, categories, products, recommendations] = await Promise.all([
    fetchBanners(),
    fetchCategories(),
    fetchProducts(),
    fetchRecommendations(),
  ]);
  // 总耗时 0.7s
}

收益:TTFB 从 2.8s → 0.7s。

Step 3:Streaming(流式渲染)

部分组件不需要等所有数据,可以先流出来:

import { Suspense } from "react";

export default function HomePage() {
  return (
    <>
      {/* 立即渲染 */}
      <Header />
      <HeroBanner />

      {/* 流式加载 */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductCarousel />
      </Suspense>

      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations />
      </Suspense>

      <Suspense fallback={<PersonalFeedSkeleton />}>
        <PersonalFeed />
      </Suspense>
    </>
  );
}

收益:用户先看到 Hero Banner(LCP 1.1s),其他模块边加载边显示。

Step 4:Client Component 最小化

我们把 Client Component 缩到极致:

// 重构前:整个页面是 Client
"use client";
function ProductCarousel() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    fetch("/api/products").then(r => r.json()).then(setProducts);
  }, []);
  // 380KB JS
}

// 重构后:只交互部分是 Client
// ProductCarousel.tsx(Client,只负责交互)
"use client";
function ProductCarousel({ products }: { products: Product[] }) {
  const [index, setIndex] = useState(0);
  // 只 8KB JS
}

// page.tsx(Server,拉数据 + 传给 Client)
async function HomePage() {
  const products = await db.products.findMany();
  return <ProductCarousel products={products} />;
}

收益:客户端 JS 从 380KB → 8KB(减少 98%)。

6 个踩坑记录

坑 1:useState 不能在 Server 用

// ❌ 错误
async function Page() {
  const [count, setCount] = useState(0);  // 报错
  return <div>{count}</div>;
}

// ✅ 正确:状态逻辑移到 Client Component
// Page.tsx (Server)
async function Page() {
  return <Counter initial={0} />;
}

// Counter.tsx (Client)
"use client";
function Counter({ initial }: { initial: number }) {
  const [count, setCount] = useState(initial);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

坑 2:第三方组件必须包一层 Client

// ❌ 错误:在 Server 里用需要 hook 的组件
import { ClientOnlyLib } from "client-only-lib";

async function Page() {
  return <ClientOnlyLib />;  // 报错
}

// ✅ 正确:包一层 Client 组件
// ClientOnlyLibWrapper.tsx
"use client";
import { ClientOnlyLib } from "client-only-lib";
export default ClientOnlyLib;

// Page.tsx (Server)
import Wrapper from "./ClientOnlyLibWrapper";
async function Page() {
  return <Wrapper />;
}

坑 3:Date 对象无法序列化

// ❌ 错误
async function Page() {
  return <ClientComponent createdAt={new Date()} />;  // 报错
}

// ✅ 正确:转字符串
async function Page() {
  return <ClientComponent createdAt={new Date().toISOString()} />;
}

坑 4:环境变量必须区分

// ❌ 错误:Server 环境变量泄漏到 Client
async function Page() {
  return <ClientComponent apiKey={process.env.API_KEY} />;  // 报错(安全)
}

// ✅ 正确:Server 用全名,Client 用 NEXT_PUBLIC_ 前缀
async function Page() {
  // Server 端可以访问 process.env.API_KEY
  const data = await fetch(process.env.INTERNAL_API_URL);
  return <ClientComponent publicKey={process.env.NEXT_PUBLIC_KEY} />;
}

坑 5:Context Provider 必须 Client

// ❌ 错误:Context Provider 在 Server 里
async function Page() {
  return (
    <MyContext.Provider value={...}>
      <Children />
    </MyContext.Provider>
  );
}

// ✅ 正确:包一层 Client
// Providers.tsx
"use client";
import { MyContext } from "./context";
export function Providers({ children, value }) {
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

// Page.tsx
async function Page() {
  const value = await fetchValue();
  return <Providers value={value}><Children /></Providers>;
}

坑 6:Next.js 缓存行为变化

RSC + Next.js 16 默认激进缓存

// 默认缓存(GET 请求结果会被缓存)
async function Page() {
  const data = await fetch("https://api.example.com/data");
  return <div>{data.title}</div>;
}

// 禁用缓存
async function Page() {
  const data = await fetch("https://api.example.com/data", { cache: "no-store" });
  return <div>{data.title}</div>;
}

// 按时间重新验证(ISR)
async function Page() {
  const data = await fetch("https://api.example.com/data", {
    next: { revalidate: 60 }  // 60 秒后重新拉取
  });
  return <div>{data.title}</div>;
}

最终成果

性能数据

指标 重构前 重构后 提升
LCP 4.2s 1.1s -74%
FID 280ms 18ms -94%
CLS 0.15 0.02 -87%
TTI 6.5s 1.8s -72%
JS Bundle 380KB 8KB -98%
Lighthouse 评分 32 89 +57

业务数据

  • 跳出率:45% → 28%
  • 首屏转化率:1.8% → 3.2%
  • SEO 流量:月 +42%
  • 服务器成本:-30%(TTFB 快 = 短连接)

我的看法

RSC 不是"另一个 SSR",而是前端架构的范式转变

  1. Server 是默认:能服务端做的不放客户端
  2. Client 只做交互:状态、事件、动画
  3. Streaming 是杀手特性:TTFB 不再是瓶颈
  4. Bundle Size 缩小 10 倍:移动端用户体验质变

未来值得关注的:

  • React 19 稳定:useActionState / useOptimistic 让交互更简单
  • View Transitions API:页面切换动画原生支持
  • Partial Prerendering (PPR):静态 + 动态混合渲染

参考


完整迁移示例代码已在文中展示,可直接在本地项目验证。

相关文章