React Server Components的运行原理
React Server Components(RSC)是React 18引入的渲染范式,将组件分为Server Components和Client Components两种类型。Server Components在服务端渲染,不打包到客户端JavaScript中,可以直接访问数据库和文件系统,零客户端体积开销。Client Components在浏览器中运行,负责交互逻辑和状态管理。两种组件通过模块系统区分,使用”use client”指令标记Client Components边界。
RSC的核心优势在于:服务端组件不增加客户端bundle大小,数据获取在服务端完成无需额外API层,组件渲染结果以序列化数据流传输到客户端。这意味着页面初始加载更快,交互水合(Hydration)后的JavaScript体积更小。
Server Components与Client Components的边界划分
确定哪些组件应该是Server Component、哪些应该是Client Component,遵循以下原则:
Server Component适用场景:数据获取和展示、静态内容渲染、需要访问后端资源的组件、无需用户交互的展示型组件。
Client Component适用场景:事件处理(onClick、onChange等)、使用useState/useReducer等Hook、使用浏览器API(window、document等)、使用第三方交互库。
// app/products/page.tsx (Server Component)
import { db } from '@/lib/db'
import ProductCard from './ProductCard'
import AddToCart from './AddToCart'
export default async function ProductsPage() {
// 直接在服务端查询数据库,无需API层
const products = await db.product.findMany({
where: { status: 'active' },
take: 20
})
return (
<div className="grid grid-cols-4 gap-4">
{products.map(product => (
<div key={product.id}>
{/* ProductCard是Server Component,服务端渲染 */}
<ProductCard product={product} />
{/* AddToCart需要onClick,是Client Component */}
<AddToCart productId={product.id} />
</div>
))}
</div>
)
}
// app/products/ProductCard.tsx (Server Component)
// 不需要"use client",默认就是Server Component
export default function ProductCard({ product }) {
return (
<div>
<h3>{product.name}</h3>
<p>{product.description}</p>
<span>¥{product.price}</span>
</div>
)
}
// app/products/AddToCart.tsx (Client Component)
'use client'
import { useState } from 'react'
export default function AddToCart({ productId }) {
const [loading, setLoading] = useState(false)
const handleAdd = async () => {
setLoading(true)
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId })
})
setLoading(false)
}
return (
<button onClick={handleAdd} disabled={loading}>
{loading ? '添加中...' : '加入购物车'}
</button>
)
}
Server Component可以导入Client Component,但Client Component不能直接导入Server Component。如果Client Component需要使用Server Component的渲染结果,可以通过children prop传递,React会自动处理这种嵌套关系。
流式SSR与Suspense边界配置
流式SSR允许服务端将已渲染好的HTML分块发送到浏览器,不必等待所有数据加载完成。Suspense组件定义了流式渲染的边界,被Suspense包裹的组件可以异步加载,先显示fallback内容,数据就绪后自动替换。
// app/dashboard/page.tsx
import { Suspense } from 'react'
import SalesChart from './SalesChart'
import UserList from './UserList'
import OrderTable from './OrderTable'
export default function DashboardPage() {
return (
<div>
<h1>仪表盘</h1>
{/* 每个Suspense边界独立流式渲染 */}
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
<Suspense fallback={<UserListSkeleton />}>
<UserList />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<OrderTable />
</Suspense>
</div>
)
}
// app/dashboard/SalesChart.tsx (Server Component)
async function getSalesData() {
// 模拟慢查询
await new Promise(resolve => setTimeout(resolve, 2000))
const res = await fetch('https://api.example.com/sales', {
cache: 'no-store' // 每次请求都重新获取
})
return res.json()
}
export default async function SalesChart() {
const data = await getSalesData()
return (
<div className="chart-container">
{/* 渲染图表 */}
{data.map(item => (
<div key={item.month}>{item.month}: {item.total}</div>
))}
</div>
)
}
function ChartSkeleton() {
return (
<div className="animate-pulse h-64 bg-gray-200 rounded">
加载图表数据中...
</div>
)
}
Suspense边界的设计原则是:将可能慢的异步操作隔离到独立的Suspense中,避免一个慢请求阻塞整个页面。用户看到的是页面逐步填充内容,而非长时间白屏等待。每个Suspense边界对应HTTP流中的一个chunk,服务端先发送骨架屏HTML,数据就绪后追加对应的组件HTML。
数据获取策略与缓存控制
React Server Components中的数据获取有几个关键配置项控制缓存行为:
// 1. 默认缓存(force-cache)
// 请求会被缓存,下次相同请求直接返回缓存结果
const data = await fetch('https://api.example.com/products')
// 等价于
const data = await fetch('https://api.example.com/products', {
cache: 'force-cache'
})
// 2. 不缓存(no-store)
// 每次请求都重新获取,适合实时性要求高的数据
const data = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
})
// 3. 重新验证(revalidate)
// 缓存数据但在指定秒数后标记为过期,下次请求触发重新获取
const data = await fetch('https://api.example.com/articles', {
next: { revalidate: 3600 } // 1小时后重新验证
})
// 4. 标签化重新验证
// 按标签管理缓存,可以手动触发某类数据的重新验证
const data = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
})
// 在Server Action或API路由中手动触发
import { revalidateTag } from 'next/cache'
async function updateProduct(id, data) {
await db.product.update({ where: { id }, data })
// 手动触发products标签的缓存失效
revalidateTag('products')
}
缓存策略的选择:静态内容用force-cache,频繁更新的数据用revalidate,实时数据用no-store,需要精确控制的用tag。混合使用时,按数据更新频率分层配置。
Client Component水合与交互注水
Server Components渲染的HTML到达浏览器后,Client Components需要进行水合(Hydration)才能响应用户交互。水合过程将服务端HTML与Client Component的JavaScript逻辑关联。React 18引入了选择性水合,优先水合用户正在交互的区域。
// 使用Suspense控制水合优先级
<Suspense fallback={<Skeleton />}>
<InteractiveWidget />
</Suspense>
// React 18还支持useId解决SSR/CSR ID不一致问题
import { useId } from 'react'
function FormField({ label }) {
const id = useId() // SSR和CSR生成相同ID
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
性能优化与Bundle分析
RSC的bundle优化体现在Server Component的代码不会出现在客户端JavaScript中。验证方法:
# 分析客户端bundle构成
# Next.js项目
ANALYZE=true npm run build
# 查看Client Component的bundle大小
# 只有标记了"use client"的组件及其依赖会打包到客户端
# 优化策略:
# 1. 将重量级库(如图表库、编辑器)隔离到Client Component
# 并用dynamic import按需加载
import dynamic from 'next/dynamic'
const RichEditor = dynamic(() => import('./RichEditor'), {
loading: () => <p>编辑器加载中...</p>,
ssr: false // 不在服务端渲染
})
// 2. Server Component中引用的库不会增加客户端bundle
// 如服务端使用moment格式化日期,moment不会打包到客户端
import moment from 'moment' // 零客户端开销
// 3. 共享代码提取到独立模块
// 纯函数和数据类型定义可被Server和Client同时使用
通过React DevTools Profiler分析组件渲染耗时,识别不必要的重渲染。Server Component的重渲染由服务端请求驱动,无需useMemo优化。Client Component的重渲染优化与常规React应用相同,使用React.memo、useMemo和useCallback减少不必要的更新。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-xuan-ran-ji-zhi-xiang-jie-liu-shi-ssr/