React Server Components 实战:从 0 到 1 重构电商首页
我们用 React Server Components 把电商首页 LCP 从 4.2s 降到 1.1s,本文分享完整迁移路径和踩坑记录。
今日技术简讯
📰 技术简讯 · 2026-07-01
今日聚合 6 条热门技术内容(7 月第一天)。
🤖 AI / LLM
1. OpenAI 开源 gpt-oss-120B,本地可运行
- 链接:https://openai.com/index/gpt-oss
- 来源:OpenAI
- 摘要:gpt-oss-120B 在消费级 GPU(24GB)上即可运行,性能接近 GPT-4 水平。
2. Anthropic 推出 Computer Use 通用版
- 链接:https://www.anthropic.com/news/computer-use
- 来源:Anthropic
- 摘要:Claude Sonnet 4.5 可控制任意桌面应用,办公自动化场景覆盖 90%。
🎨 前端 / 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)
↓
客户端再发请求拉个性化数据
问题:
- 12 个串行 API 调用,TTFB 2.8s
- 整个页面作为 Client Component,380KB JS 在低端机上跑 5s+
- 首屏内容(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",而是前端架构的范式转变:
- Server 是默认:能服务端做的不放客户端
- Client 只做交互:状态、事件、动画
- Streaming 是杀手特性:TTFB 不再是瓶颈
- Bundle Size 缩小 10 倍:移动端用户体验质变
未来值得关注的:
- React 19 稳定:useActionState / useOptimistic 让交互更简单
- View Transitions API:页面切换动画原生支持
- Partial Prerendering (PPR):静态 + 动态混合渲染
参考
完整迁移示例代码已在文中展示,可直接在本地项目验证。