React Server Components 实战:我重构了一个 Next.js 应用,性能提升了 3 倍
Next.js 14 之后,Server Components 成为默认。但很多人还在用老思路写代码,白白浪费了性能优势。
我花了一周时间重构了一个电商应用,LCP 从 3.2s 降到 1.1s。
核心概念
Server Components vs Client Components
| 特性 | Server Component | Client Component |
|------|-----------------|------------------|
| 运行环境 | 服务器 | 浏览器 |
| 可以使用 | 数据库、文件系统 | useState、useEffect |
| JS Bundle | 不发送到客户端 | 发送到客户端 |
| 交互性 | 无 | 有 |
判断标准
需要交互? → Client Component
需要浏览器 API? → Client Component
需要实时数据? → Client Component
其他 → Server Component重构前的问题
问题 1:所有组件都是 Client Component
// ❌ 重构前:整个页面都是客户端渲染
'use client';
export default function ProductPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) return <Skeleton />;
return (
<div>
<Header />
<ProductList products={products} />
<Footer />
</div>
);
}问题:
- 首屏需要等待 JS 加载和执行
- 所有组件代码都发送到客户端
- SEO 不友好
问题 2:数据获取在客户端
// ❌ 重构前
'use client';
function ProductList({ products }) {
const [reviews, setReviews] = useState({});
useEffect(() => {
// 每个产品都要单独请求
products.forEach(async (product) => {
const res = await fetch(`/api/reviews/${product.id}`);
const data = await res.json();
setReviews(prev => ({ ...prev, [product.id]: data }));
});
}, [products]);
return products.map(product => (
<ProductCard
key={product.id}
product={product}
reviews={reviews[product.id]}
/>
));
}问题:
- N+1 请求问题
- 瀑布式加载
- 用户体验差
重构后的代码
方案 1:Server Component 直接获取数据
// ✅ 重构后:服务器端获取数据
import { db } from '@/lib/db';
import { ProductList } from './ProductList';
export default async function ProductPage() {
// 直接在服务器端获取数据
const products = await db.products.findMany({
include: { reviews: true },
take: 20
});
return (
<div>
<Header />
<ProductList products={products} />
<Footer />
</div>
);
}优势:
- 数据在服务器端获取,无网络延迟
- 无 JS Bundle 增加
- SEO 友好
方案 2:混合使用 Server 和 Client Components
// ✅ 重构后:服务器组件
import { db } from '@/lib/db';
import { ProductCard } from './ProductCard';
import { AddToCartButton } from './AddToCartButton';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.products.findUnique({
where: { id: params.id },
include: { reviews: true, variants: true }
});
if (!product) {
notFound();
}
return (
<div>
{/* 服务器组件:展示产品信息 */}
<ProductInfo product={product} />
{/* 服务器组件:展示评论 */}
<ReviewList reviews={product.reviews} />
{/* 客户端组件:需要交互 */}
<AddToCartButton productId={product.id} />
{/* 客户端组件:图片轮播 */}
<ImageGallery images={product.images} />
</div>
);
}
// ✅ 客户端组件:只包含交互逻辑
'use client';
export function AddToCartButton({ productId }: { productId: string }) {
const [adding, setAdding] = useState(false);
const handleAdd = async () => {
setAdding(true);
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId })
});
setAdding(false);
};
return (
<button onClick={handleAdd} disabled={adding}>
{adding ? '添加中...' : '加入购物车'}
</button>
);
}方案 3:流式渲染
// ✅ 使用 Suspense 实现流式渲染
import { Suspense } from 'react';
export default function ProductPage() {
return (
<div>
<Header />
{/* 立即显示骨架屏 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails />
</Suspense>
{/* 评论稍后加载 */}
<Suspense fallback={<ReviewSkeleton />}>
<Reviews />
</Suspense>
{/* 推荐商品最后加载 */}
<Suspense fallback={<RecommendationSkeleton />}>
<Recommendations />
</Suspense>
<Footer />
</div>
);
}
// 每个组件独立获取数据
async function ProductDetails() {
const product = await db.products.findUnique({ ... });
return <div>{/* 渲染产品 */}</div>;
}
async function Reviews() {
const reviews = await db.reviews.findMany({ ... });
return <div>{/* 渲染评论 */}</div>;
}
async function Recommendations() {
const recommendations = await getRecommendations();
return <div>{/* 渲染推荐 */}</div>;
}性能对比
| 指标 | 重构前 | 重构后 | 提升 |
|------|--------|--------|------|
| LCP | 3.2s | 1.1s | 65% |
| FID | 150ms | 50ms | 67% |
| CLS | 0.15 | 0.05 | 67% |
| JS Bundle | 450KB | 180KB | 60% |
| TTFB | 800ms | 200ms | 75% |
最佳实践
1. 组件拆分原则
// ✅ 好的拆分
// Server Component
export default async function Page() {
const data = await fetchData();
return (
<div>
<StaticContent data={data} />
<InteractivePart id={data.id} />
</div>
);
}
// Client Component - 只包含交互
'use client';
export function InteractivePart({ id }: { id: string }) {
const [state, setState] = useState();
return <button onClick={() => setState(!state)}>Toggle</button>;
}2. 避免客户端组件嵌套
// ❌ 不好:客户端组件嵌套太深
'use client';
export function Parent() {
return (
<div>
<ChildA /> {/* 也是客户端组件 */}
<ChildB /> {/* 也是客户端组件 */}
</div>
);
}
// ✅ 好:把数据传给客户端组件
export default async function Page() {
const data = await fetchData();
return (
<ClientWrapper data={data}>
<ServerChildA data={data.a} />
<ServerChildB data={data.b} />
</ClientWrapper>
);
}3. 使用 Server Actions
// ✅ 使用 Server Actions 替代 API Routes
'use server';
export async function addToCart(productId: string) {
const session = await getSession();
if (!session) {
throw new Error('Not authenticated');
}
await db.cartItems.create({
data: { userId: session.userId, productId }
});
revalidatePath('/cart');
}
// 在客户端组件中使用
'use client';
import { addToCart } from './actions';
export function AddToCartButton({ productId }: { productId: string }) {
return (
<form action={addToCart.bind(null, productId)}>
<button type="submit">加入购物车</button>
</form>
);
}4. 缓存策略
// 使用 React cache 避免重复获取
import { cache } from 'react';
export const getProduct = cache(async (id: string) => {
return db.products.findUnique({ where: { id } });
});
// 在多个组件中使用,只会获取一次
async function ProductInfo({ id }: { id: string }) {
const product = await getProduct(id);
return <div>{product.name}</div>;
}
async function ProductReviews({ id }: { id: string }) {
const product = await getProduct(id); // 使用缓存
return <div>{product.reviews.length} 条评论</div>;
}常见错误
错误 1:在 Server Component 中使用 useState
// ❌ 错误
export default function Page() {
const [count, setCount] = useState(0); // 不能在 Server Component 中使用
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 正确
'use client';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}错误 2:忘记 'use client' 指令
// ❌ 错误:使用了浏览器 API 但没有标记为客户端组件
export function ThemeToggle() {
const theme = localStorage.getItem('theme'); // 服务器端没有 localStorage
return <div>{theme}</div>;
}
// ✅ 正确
'use client';
export function ThemeToggle() {
const [theme, setTheme] = useState(() => localStorage.getItem('theme'));
return <div>{theme}</div>;
}总结
Server Components 的核心原则:
1. 默认使用 Server Component
2. 只在需要交互时使用 Client Component
3. 数据获取放在服务器端
4. 使用 Suspense 实现流式渲染
5. 避免客户端组件嵌套过深
遵循这些原则,性能可以提升 3 倍以上。
重构时间:2026年7月
项目规模:电商应用,50+ 页面
性能提升:LCP 3.2s → 1.1s
#React #ServerComponents #NextJS #性能优化
读者评论 3