React Server Components(RSC)是React 18引入的渲染范式,将组件拆分为服务端组件和客户端组件,服务端组件在服务器上执行不发送JavaScript到浏览器。Next.js App Router是对RSC的完整工程化实现。本文解析RSC工作原理,给出Next.js App Router的实践方案。
React Server Components运行机制与渲染流程
传统SSR在服务端渲染HTML,但所有组件的JavaScript仍然发送到客户端进行hydration。RSC不同——服务端组件的代码不包含在客户端bundle中,只有客户端组件的JavaScript会发送到浏览器。
// app/products/page.tsx - 服务端组件(默认)
// 文件顶部无需 "use client" 声明
import { db } from '@/lib/db'
import ProductCard from './product-card'
import { Suspense } from 'react'
// 服务端组件可直接访问数据库,无需API层
async function getProducts() {
const products = await db.product.findMany({
orderBy: { createdAt: 'desc' },
take: 20
})
return products
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div className="grid grid-cols-3 gap-6">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
渲染过程中React Server为每个服务端组件生成一个序列化的RSC Payload,包含组件树结构和数据。客户端组件通过引用标记嵌入到RSC Payload中,浏览器收到Payload后渲染客户端组件并执行交互逻辑。服务端组件可以使用async/await直接获取数据,不需要useEffect和loading states。
客户端组件与服务端组件的边界划分
使用”use client”指令声明客户端组件。只有需要交互(onClick、useState、useEffect等)的组件才标记为客户端组件:
// app/products/product-card.tsx
"use client"
import { useState } from 'react'
import { useRouter } from 'next/navigation'
interface ProductCardProps {
product: {
id: string
name: string
price: number
image: string
}
}
export default function ProductCard({ product }: ProductCardProps) {
const [isFavorited, setIsFavorited] = useState(false)
const router = useRouter()
const handleFavorite = async () => {
setIsFavorited(!isFavorited)
await fetch(`/api/favorites/${product.id}`, {
method: isFavorited ? 'DELETE' : 'POST',
})
}
return (
<div className="border rounded-lg p-4">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>¥{product.price}</p>
<button onClick={handleFavorite}>
{isFavorited ? '已收藏' : '收藏'}
</button>
</div>
)
}
组件边界的核心原则是将”use client”尽量下推到叶子节点。一个服务端组件可以渲染客户端组件,客户端组件也可以从服务端组件接收children prop。这种组合模式减少了客户端JavaScript体积:
// app/layout.tsx - 根布局(服务端组件)
import { Analytics } from './analytics' // 客户端组件
import Header from './header'
export default function RootLayout({ children }) {
return (
<html>
<body>
<Header />
{/* children来自服务端渲染的页面 */}
{children}
{/* Analytics的JS发送到客户端,但不影响页面内容渲染 */}
<Analytics />
</body>
</html>
)
}
Next.js App Router数据获取与缓存策略
App Router中fetch默认启用缓存,通过cache选项控制缓存行为:
// 完全静态化 - 构建时获取,缓存到CDN
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache' // 默认行为
})
// 每次请求重新获取 - 不缓存
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// ISR增量静态再生 - 60秒后后台重新生成
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// 路由段级配置
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // ISR每小时重新生成
export const dynamic = 'force-static' // 强制静态
export const fetchCache = 'default-cache'
export default async function BlogPost({ params }) {
const post = await fetch(
`https://api.example.com/posts/${params.slug}`,
{ next: { tags: ['blog-post', `post-${params.slug}`] } }
).then(r => r.json())
return <article>{post.content}</article>
}
流式渲染与Suspense边界配置
App Router通过Suspense实现流式渲染,将页面分割为多个独立的流式块,先渲染的部分立即发送到浏览器:
// app/dashboard/page.tsx
import { Suspense } from 'react'
import RevenueChart from './revenue-chart'
import UserTable from './user-table'
import StatsCards from './stats-cards'
export default function Dashboard() {
return (
<div>
{/* StatsCards先加载,立即渲染 */}
<Suspense fallback={<SkeletonGrid />}>
<StatsCards />
</Suspense>
{/* RevenueChart较慢,不阻塞UserTable */}
<div className="grid grid-cols-2 gap-6 mt-6">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<UserTable />
</Suspense>
</div>
</div>
)
}
// 慢查询组件 - 模拟延迟
async function RevenueChart() {
const data = await fetch('https://api.example.com/revenue', {
cache: 'no-store'
}).then(r => r.json())
return <Chart data={data} />
}
每个Suspense边界独立流式渲染,浏览器收到第一个chunk就开始渲染对应区域。相比传统SSR等待整个页面数据加载完成再发送HTML,流式渲染显著改善首屏内容可绘制时间(FCP)和最大内容绘制时间(LCP)。
实测一个包含3个数据源的Dashboard页面,传统SSR模式下FCP约2.8秒,流式渲染模式下FCP降到0.6秒(StatsCards先渲染),LCP从3.2秒降到1.3秒。关键是Suspense边界要按照数据获取速度分层级,快查询放在外层先渲染,慢查询放在内层延迟渲染。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-fu-wu-duan-xuan-ran-jia-gou-yu/