React Server Components实战:提升首屏渲染性能
大家好,我是蔓蔓。最近我在一个新项目中尝试了React Server Components(RSC),体验非常好。今天我来和大家分享React Server Components的核心概念和实战经验。
什么是React Server Components
核心概念
React Server Components是React团队推出的一种新的组件类型,它允许组件在服务器端渲染,然后直接发送到客户端。
核心优势:
- 减少JavaScript包体积:服务器组件不会发送到客户端
- 直接访问后端数据:可以在组件中直接调用数据库
- 更快的首屏加载:减少网络请求和渲染时间
Server Components vs Client Components
| 特性 | Server Components | Client Components |
|---|---|---|
| 运行位置 | 服务器 | 客户端 |
| JavaScript包 | 不包含在bundle中 | 包含在bundle中 |
| 访问后端 | 直接访问 | 通过API访问 |
| 交互能力 | 无(纯渲染) | 有(事件处理) |
| 状态管理 | 无 | 有(useState, useEffect) |
项目配置
环境要求
json
{
"react": "^18.0.0",
"react-dom": "^18.0.0",
"next": "^13.0.0"
}
目录结构
├── app/
│ ├── layout.jsx # 布局组件
│ ├── page.jsx # 页面组件(Server Component)
│ └── components/
│ ├── ServerComponent.jsx # 服务器组件
│ └── ClientComponent.jsx # 客户端组件
组件标识
javascript
// 服务器组件(默认)
// app/components/ServerComponent.jsx
async function ServerComponent() {
const data = await fetchData();
return <div>{data}</div>;
}
// 客户端组件(需要声明)
// app/components/ClientComponent.jsx
'use client';
function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
实战案例
案例1:数据获取
javascript
// app/page.jsx - Server Component
async function BlogPage() {
// 直接在组件中获取数据
const posts = await fetch('/api/posts').then(res => res.json());
return (
<div>
<h1>博客列表</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</li>
))}
</ul>
</div>
);
}
export default BlogPage;
案例2:嵌套组件
javascript
// app/components/PostList.jsx - Server Component
async function PostList() {
const posts = await fetch('/api/posts').then(res => res.json());
return (
<ul>
{posts.map(post => (
<PostItem key={post.id} post={post} />
))}
</ul>
);
}
// app/components/PostItem.jsx - Server Component
function PostItem({ post }) {
return (
<li>
<h3>{post.title}</h3>
<p>{post.content}</p>
{/* 客户端组件用于交互 */}
<LikeButton postId={post.id} />
</li>
);
}
// app/components/LikeButton.jsx - Client Component
'use client';
import { useState } from 'react';
function LikeButton({ postId }) {
const [likes, setLikes] = useState(0);
const handleLike = async () => {
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
setLikes(l => l + 1);
};
return (
<button onClick={handleLike}>
👍 {likes}
</button>
);
}
案例3:缓存策略
javascript
// app/components/ProductList.jsx - Server Component
import { unstable_cache } from 'next/cache';
// 创建缓存函数
const getProducts = unstable_cache(
async (category) => {
console.log('Fetching products...');
const res = await fetch(`/api/products?category=${category}`);
return res.json();
},
['products'], // 缓存键
{ revalidate: 3600 } // 1小时重新验证
);
async function ProductList({ category }) {
const products = await getProducts(category);
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
性能优化
并行数据获取
javascript
// 并行获取多个数据源
async function Dashboard() {
// 使用 Promise.all 并行获取
const [user, orders, notifications] = await Promise.all([
fetch('/api/user').then(res => res.json()),
fetch('/api/orders').then(res => res.json()),
fetch('/api/notifications').then(res => res.json())
]);
return (
<div>
<UserProfile user={user} />
<OrderList orders={orders} />
<NotificationList notifications={notifications} />
</div>
);
}
流式渲染
javascript
// app/components/CommentList.jsx - Server Component
async function* generateComments(postId) {
const comments = await fetch(`/api/posts/${postId}/comments`).then(res => res.json());
for (const comment of comments) {
yield <CommentItem key={comment.id} comment={comment} />;
// 模拟延迟,展示流式效果
await new Promise(resolve => setTimeout(resolve, 100));
}
}
async function CommentList({ postId }) {
return (
<div>
<Suspense fallback={<div>Loading comments...</div>}>
<StreamingComments postId={postId} />
</Suspense>
</div>
);
}
// 使用 Suspense 实现流式渲染
async function StreamingComments({ postId }) {
const commentsGenerator = generateComments(postId);
const comments = [];
for await (const comment of commentsGenerator) {
comments.push(comment);
}
return <>{comments}</>;
}
错误边界
javascript
// app/components/ErrorBoundary.jsx - Client Component
'use client';
import { useState, useEffect } from 'react';
function ErrorBoundary({ children, fallback }) {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const handleError = (e) => {
setHasError(true);
setError(e.error);
return true;
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, []);
if (hasError) {
return <fallback error={error} />;
}
return children;
}
// 使用示例
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ServerComponent />
</ErrorBoundary>
最佳实践
组件拆分策略
- 服务器组件:数据获取、纯展示、静态内容
- 客户端组件:交互逻辑、状态管理、事件处理
javascript
// 推荐:将数据获取和展示分离
async function ProductPage({ id }) {
// 服务器组件:数据获取
const product = await fetch(`/api/products/${id}`).then(res => res.json());
return (
<div>
{/* 服务器组件:纯展示 */}
<ProductInfo product={product} />
{/* 客户端组件:交互功能 */}
<AddToCartButton productId={id} />
<ProductReviews productId={id} />
</div>
);
}
避免常见陷阱
javascript
// ❌ 错误:在服务器组件中使用 useState
async function BadComponent() {
const [count, setCount] = useState(0); // 错误!
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 正确:将交互逻辑移到客户端组件
async function GoodComponent() {
const data = await fetchData();
return (
<div>
<DisplayData data={data} />
<InteractiveButton /> {/* 客户端组件 */}
</div>
);
}
// ❌ 错误:在服务器组件中使用浏览器API
async function BadComponent() {
const width = window.innerWidth; // 错误!
return <div>Width: {width}</div>;
}
// ✅ 正确:使用 useEffect 获取浏览器信息
'use client';
function GoodComponent() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>Width: {width}</div>;
}
总结
React Server Components带来了以下好处:
- 更小的JavaScript包:服务器组件不会被发送到客户端
- 更快的首屏加载:减少网络请求和渲染时间
- 简化数据获取:可以直接在组件中获取数据
- 更好的SEO:服务器端渲染对搜索引擎更友好
但也需要注意:
- 服务器组件不能使用状态管理和浏览器API
- 需要合理拆分服务器组件和客户端组件
- 需要考虑缓存策略和错误处理
技术应当有温度,React Server Components通过减少JavaScript包体积和加快首屏加载,为用户带来更好的体验。
你在使用React Server Components方面有什么经验?欢迎在评论区交流~