React Server Components(RSC)是React 18引入的架构范式,将组件按执行环境分为Server Components和Client Components。RSC不是简单的SSR替代方案,而是改变了组件的渲染模型:服务端组件在服务器上执行且永不发送到客户端,客户端组件按需水合。Next.js App Router已全面采用RSC,理解其渲染流程和流式传输机制对构建高性能Web应用至关重要。
Server Components与Client Components的边界划分
RSC的核心约束是组件树自上而下的执行模型。默认所有组件都是Server Components,只有在文件顶部声明”use client”的才是Client Components。
关键规则:
– Server Components可以导入Client Components
– Client Components不能直接导入Server Components(但可以将Server Components作为children传入)
– Server Components可以使用async/await直接读取数据库或调用API
– Client Components可以使用useState、useEffect等hooks和浏览器API
// app/page.tsx - Server Component (默认)
import { db } from '@/lib/db'
import InteractiveChart from './InteractiveChart'
export default async function Page() {
// 直接在服务端查询数据库,无需API层
const data = await db.query('SELECT * FROM metrics LIMIT 100')
return (
<div>
<h1>Metrics Dashboard</h1>
{/* Server Component渲染静态内容,不发送到客户端 */}
<table>
{data.map(row => (
<tr key={row.id}><td>{row.name}</td><td>{row.value}</td></tr>
))}
</table>
{/* Client Component处理交互逻辑 */}
<InteractiveChart data={data} />
</div>
)
}
// app/InteractiveChart.tsx
'use client'
import { useState } from 'react'
export default function InteractiveChart({ data }) {
const [filter, setFilter] = useState('')
const filtered = data.filter(d => d.name.includes(filter))
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{/* 渲染图表 */}
</div>
)
}
RSC渲染管线与序列化协议
RSC的渲染过程分为两个阶段:服务端渲染和客户端水合。
服务端将Server Component树渲染为特殊的RSC Payload(非HTML),格式是一种行分隔的序列化流:
// RSC Payload示例(简化)
0:["$","div",null,{"children":[
["$","h1",null,{"children":"Metrics Dashboard"}],
["$","table",null,{"children":[
["$","tr",null,{"children":[
["$","td",null,{"children":"CPU Usage"}],
["$","td",null,{"children":"87%"}]
]}]
]}],
["$","$L","app/InteractiveChart.tsx",{"data":[...]}]
]}]
其中”$L”是Client Component的引用标记,客户端收到此标记后动态加载对应模块并渲染。Server Component的内容直接内联在payload中,不包含任何JS代码。
RSC Payload的优势在于它是可流式传输的。服务端不需要等待所有数据就绪才开始发送,先发送已准备好的部分,数据未就绪的组件用Suspense包裹。
流式SSR与Suspense边界配合
Next.js App Router的流式渲染基于Suspense实现。当Server Component内部有异步操作时,用Suspense包裹会在payload中生成一个fallback块,服务端先发送fallback,数据就绪后再发送真实内容替换:
// app/dashboard/layout.tsx
import { Suspense } from 'react'
export default function DashboardLayout({ children }) {
return (
<div>
<header>Navigation (立即发送)</header>
<Suspense fallback={<div>Loading chart...</div>}>
{/* 慢查询组件 */}
<SlowDataComponent />
</Suspense>
<Suspense fallback={<div>Loading table...</div>}>
<DataTable />
</Suspense>
{children}
</div>
)
}
// app/dashboard/SlowDataComponent.tsx
export default async function SlowDataComponent() {
// 模拟耗时查询
const data = await fetch('https://api.example.com/slow', {
next: { revalidate: 60 }
}).then(r => r.json())
return <Chart data={data} />
}
浏览器收到的HTML流:
<header>Navigation</header>
<div>Loading chart...</div>
<div>Loading table...</div>
<!-- ...后续HTML流... -->
<template><div>真实图表内容</div></template>
用户在页面加载的瞬间就能看到导航和loading状态,而非白屏等待所有数据。这在Core Web Vitals指标上直接降低LCP(最大内容绘制时间)和改善CLS(累积布局偏移)。
数据获取模式与缓存策略
RSC中数据获取有三个层级:请求时获取、构建时获取、增量静态再生成(ISR)。
// 请求时获取(每次请求都执行)
async function getRealtimeData() {
const res = await fetch('https://api.example.com/realtime', {
cache: 'no-store'
})
return res.json()
}
// 构建时获取(静态生成)
async function getStaticData() {
const res = await fetch('https://api.example.com/static', {
cache: 'force-cache'
})
return res.json()
}
// ISR:构建时缓存,每60秒过期重新生成
async function getISRData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return res.json()
}
// 按需重新验证(On-Demand Revalidation)
async function getOnDemandData() {
const res = await fetch('https://api.example.com/data', {
next: { tags: ['metrics'] }
})
return res.json()
}
// 通过API触发重新验证
import { revalidateTag } from 'next/cache'
export async function POST() {
revalidateTag('metrics')
return Response.json({ revalidated: true })
}
Client与Server组件数据传递约束
Server Component传递给Client Component的props必须可序列化。函数、Date对象、Map/Set等不能直接传递:
// 错误:传递不可序列化的值
<ClientComponent
onClick={() => {}} // 函数不能传
date={new Date()} // Date对象不能传,需转ISO字符串
data={new Map()} // Map不能传
/>
// 正确做法
<ClientComponent
dateString={new Date().toISOString()}
data={Array.from(map.entries())}
/>
// 如果需要传递函数,使用Server Action
'use server'
export async function handleSubmit(formData) {
const name = formData.get('name')
await db.insert({ name })
revalidatePath('/')
}
'use client'
import { handleSubmit } from '@/app/actions'
export function Form() {
return (
<form action={handleSubmit}>
<input name="name" />
<button type="submit">Submit</button>
</form>
)
}
RSC与传统SSR的对比
传统SSR(如Next.js Pages Router的getServerSideProps)在服务端渲染完整HTML,客户端水合时需下载整个页面的JS代码。RSC的关键区别:
1. JS包体积:RSC不发送Server Component的JS代码到客户端,仅发送Client Component代码。一个数据密集型页面可能包含10个组件,如果8个是Server Component,客户端只需下载2个组件的JS。
2. 数据获取层级:SSR的数据获取在页面级(getServerSideProps),RSC中每个组件可独立获取数据,Suspense实现并行获取和流式渲染。
3. 水合范围:SSR水合整个页面,RSC只水合Client Components,Server Components保持服务端渲染状态无需水合。
4. 导航行为:SSR每次导航都是完整页面请求,RSC在客户端导航时仅请求RSC Payload(轻量JSON),不重新下载HTML和JS。
性能优化实践与常见陷阱
1. 避免不必要的”use client”:每添加一个Client Component边界都意味着该组件及子树代码会发送到客户端。将交互逻辑尽量下沉到叶子组件。
// 不推荐:整个列表声明为Client Component
'use client'
export function ProductList({ products }) {
const [sort, setSort] = useState('price')
return (
<div>
<select onChange={...}>...</select>
{sortedProducts.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
// 推荐:仅交互部分声明Client Component,列表项保持Server Component
export function ProductList({ products }) {
return (
<div>
<SortControl /> {/* 'use client' */}
{products.map(p => <ProductCard key={p.id} product={p} />)}
</div>
)
}
2. 合理使用next/dynamic减少首屏JS体积:
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <div>Loading...</div>,
ssr: false
})
3. 警惕Server Action滥用:每个Server Action都是独立的RPC端点,过度使用会导致API碎片化。对于复杂业务逻辑,保持传统API路由更清晰。
4. 缓存层级:Next.js有四层缓存——Data Cache(fetch缓存)、Full Route Cache(路由缓存)、Router Cache(客户端导航缓存)、Request Memoization(同请求内去重)。理解各层作用域才能正确配置revalidate和tags。
5. 调试RSC:在开发环境中观察Server/Client Component边界,可在next.config.js中开启调试:
module.exports = {
experimental: {
reactOwnerStacks: true
}
}
控制台中Server Component的错误堆栈包含服务端调用链,Client Component的错误堆栈在浏览器中查看。两者混合时需要同时检查服务端日志和浏览器控制台。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/reactservercomponents-fu-wu-duan-zu-jian-xuan-ran-yuan-li/