React Server Components 实战:下一代 SSR 开发指南

核心理念
- 组件在服务器渲染并直接输出可序列化的 UI 树片段,客户端仅为交互部分注水
- 数据靠近服务器与数据库,避免瀑布式请求与冗余水合
- 按需把交互组件标记为客户端组件,减小包体与提升首屏
技术栈与初始化
bash
pnpm create next-app@latest rsc-app --ts --eslint --tailwind --app
cd rsc-app && pnpm dev
目录(App Router):
rsc-app/
app/
layout.tsx
page.tsx
components/
RepoList.tsx
InteractiveCounter.tsx
api/
items/route.ts
actions.ts
lib/
db.ts
next.config.js
Server vs Client 组件
- Server Component:默认;可访问数据库/后端,仅输出可序列化内容
- Client Component:文件顶部加
use client;可使用状态与事件,不能直接访问服务器资源
Server 组件示例
app/components/RepoList.tsx
tsx
import { cache } from 'react'
const fetchRepos = cache(async () => {
const res = await fetch('https://api.github.com/orgs/vercel/repos', { next: { revalidate: 60 } })
return res.json() as Promise<Array<{ id:number; name:string }>>
})
export default async function RepoList() {
const repos = await fetchRepos()
return (
<ul>{repos.slice(0, 10).map(r => <li key={r.id}>{r.name}</li>)}</ul>
)
}
Client 组件示例
app/components/InteractiveCounter.tsx
tsx
'use client'
import { useState } from 'react'
export default function InteractiveCounter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>Count: {n}</button>
}
页面与布局
app/page.tsx
tsx
import RepoList from './components/RepoList'
import InteractiveCounter from './components/InteractiveCounter'
export default function Page() {
return (
<main>
<h1>RSC Demo</h1>
<RepoList />
<InteractiveCounter />
</main>
)
}
app/layout.tsx
tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}
Streaming 与 Suspense
app/page.tsx
tsx
import { Suspense } from 'react'
import RepoList from './components/RepoList'
export default function Page() {
return (
<main>
<h1>Streaming</h1>
<Suspense fallback={<p>Loading repos...</p>}>
{/* Server 组件可流式传输 */}
{/* @ts-expect-error Async Server Component */}
<RepoList />
</Suspense>
</main>
)
}
数据获取与缓存策略
fetch:next: { revalidate }控制增量静态化;cache: 'no-store'关闭缓存cache(asyncFn):稳定函数层复用结果,避免重复 IOrevalidateTag/revalidatePath:配合标签或路径精细失效
app/actions.ts
tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function createItem(formData: FormData) {
// 写入数据库...
revalidateTag('items')
}
Server 组件读取:
tsx
const res = await fetch('https://api.example.com/items', { next: { tags: ['items'], revalidate: 120 } })
Server Actions(无 API 层的直接提交)
app/actions.ts
tsx
'use server'
export async function addTodo(prev: any, formData: FormData) {
const title = String(formData.get('title') ?? '')
// 写库...
return { ok: true, title }
}
app/page.tsx
tsx
import { addTodo } from './actions'
export default function Page() {
return (
<form action={addTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
)
}
Route Handlers(API 路由)
app/api/items/route.ts
ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json([{ id: 1, name: 'A' }], { status: 200 })
}
export async function POST(req: Request) {
const body = await req.json()
return NextResponse.json({ ok: true, body }, { status: 201 })
}
Edge Runtime(可选)
app/api/hello/route.ts
ts
export const runtime = 'edge'
export async function GET() { return new Response('Hi from edge') }
SEO 与 Metadata
app/page.tsx
tsx
export const metadata = { title: 'RSC 指南', description: '下一代 SSR 与流式渲染' }
常见坑与修复
- 第三方库需要 DOM 时必须在客户端组件使用
- Server 组件不可使用状态/副作用;事件处理放入客户端组件
- 仅传递可序列化 props;避免函数与类实例穿越边界
- 注意缓存层级与失效策略,避免陈旧数据
测试与调试建议
- 组件分层测试:Server 组件快照 + 客户端交互用
@testing-library/react - 端到端流式校验:使用 Playwright 验证 Suspense 与流式内容
- 观测:结合日志与 APM,对边缘与 Node 运行时区分指标
部署与性能
- Vercel 一键部署支持流式 SSR;或 Node 自托管
- 按路由分层设置
revalidate,减少后端压力 - 大列表分页与骨架屏结合,兼顾渲染与交互
迁移提示
- Page Router → App Router:拆分 Server/Client 组件,数据上移到服务器
- 移出全局客户端状态,保留必要交互在客户端
总结
- RSC 通过服务器端组件与流式输出来提升渲染性能与开发体验
- 与 Server Actions、Route Handlers 配合,形成端到端的数据与渲染闭环
- 按需选择客户端组件边界,减少包体与水合成本
数据库直连与增删改查
lib/db.ts
ts
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()
app/items/page.tsx
tsx
import { prisma } from '@/lib/db'
export default async function ItemsPage() {
const items = await prisma.item.findMany({ take: 20 })
return <ul>{items.map(i => <li key={i.id}>{i.title}</li>)}</ul>
}
app/items/actions.ts
tsx
'use server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function addItem(prev: any, formData: FormData) {
const title = String(formData.get('title') || '')
await prisma.item.create({ data: { title } })
revalidatePath('/items')
return { ok: true }
}
动态路由与静态生成
app/items/[id]/page.tsx
tsx
import { prisma } from '@/lib/db'
export async function generateStaticParams() {
const ids = await prisma.item.findMany({ select: { id: true }, take: 10 })
return ids.map(i => ({ id: String(i.id) }))
}
export default async function ItemDetail({ params }: { params: { id: string } }) {
const item = await prisma.item.findUnique({ where: { id: Number(params.id) } })
if (!item) return null
return <div>{item.title}</div>
}
并行与拦截路由
并行路由与拦截路由适用于复杂布局与模态场景。示例结构:
app/
@feed/page.tsx
@sidebar/page.tsx
intercept/(.)modal/[id]/page.tsx
乐观更新与表单状态
app/items/client/AddForm.tsx
tsx
'use client'
import { useOptimistic, useTransition } from 'react'
import { addItem } from '../actions'
export default function AddForm() {
const [list, addOptimistic] = useOptimistic<string[], string>((state, v) => [v, ...state])
const [pending, start] = useTransition()
return (
<form action={(fd) => start(async () => { addOptimistic(String(fd.get('title'))); await addItem({}, fd) })}>
<input name="title" />
<button disabled={pending} type="submit">Add</button>
<ul>{list.map((t, i) => <li key={i}>{t}</li>)}</ul>
</form>
)
}
文件上传(Server Actions)
app/upload/actions.ts
tsx
'use server'
export async function upload(prev: any, formData: FormData) {
const file = formData.get('file') as File
const buf = Buffer.from(await file.arrayBuffer())
return { size: buf.length }
}
错误与 404 边界
app/error.tsx
tsx
'use client'
export default function Error({ error }: { error: Error }) { return <div>Error</div> }
app/not-found.tsx
tsx
export default function NotFound() { return <div>Not Found</div> }
缓存失效矩阵
- 静态增量:
next: { revalidate: N } - 标签失效:
next: { tags: ['t'] }配合revalidateTag('t') - 路径失效:
revalidatePath('/p') - 禁用缓存:
cache: 'no-store'
性能与观测
- Streaming 与 Suspense 缩短 TTFB 与提升可感知速度
- 边缘运行适用于低延迟与全球分发
- 日志与指标区分 Node 与 Edge,关注错误率与渲染耗时
安全建议
- 仅在服务器组件访问密钥与数据库
- 传递可序列化数据,避免函数与实例泄露
- 对输入进行校验与清理,避免注入
架构对比与边界清单
- CSR:数据拉取在客户端,水合成本高;适合强交互页面
- 传统 SSR:整页水合,数据靠 API 层;适合内容页与 SEO
- RSC:数据与渲染上移到服务器,仅交互组件注水;适合大多数内容/混合场景
- 边界检查:
- 仅在客户端组件使用
useState/useEffect - Server 组件禁止使用浏览器 API 与事件处理
- Props 必须可序列化(JSON 可表达)
- 缓存与失效路径明确(标签/路径/时间)
- 仅在客户端组件使用
迁移步骤(增量采纳)
- 迁入 App Router,保留原 CSR/SSR 页于
pages/,新页在app/ - 拆分数据读取到 Server 组件(或 Route Handler/Server Action)
- 将交互模块标记为
use client并最小化客户端边界 - 加入 Streaming 与 Suspense,分层渲染慢数据
- 引入缓存与失效矩阵(时间/标签/路径),消除冗余请求
- 建立错误边界与 404,完善观测与告警
路由段配置与动态性
app/items/[id]/page.tsx
tsx
export const dynamic = 'force-static' // 或 'force-dynamic'
export const revalidate = 300 // 段级增量静态化
export const fetchCache = 'default-cache' // 控制 fetch 缓存
Middleware(鉴权示例)
middleware.ts
ts
import { NextResponse } from 'next/server'
export function middleware(req: Request) {
const url = new URL(req.url)
const token = (req as any).cookies?.get('token')?.value
if (url.pathname.startsWith('/dashboard') && !token) return NextResponse.redirect(new URL('/', url))
return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*'] }
generateMetadata(动态 SEO)
app/items/[id]/page.tsx
tsx
export async function generateMetadata({ params }: { params: { id: string } }) {
return { title: `Item #${params.id}`, description: 'RSC 动态元数据' }
}
观测与日志(instrumentation)
instrumentation.ts
ts
export async function register() {
console.log('app start')
}
测试套件与基准
- 单测:Server 组件快照;Client 组件交互用
@testing-library/react - E2E:Playwright 校验 Suspense 占位与流式插入
- 性能基准:记录 TTFB、LCP、交互就绪(INP),分路由对比 CSR/SSR/RSC
常见错误与修复
- 在 Server 组件使用
useState:改为客户端组件或上移为 props - 传递函数/类实例作为 props:改为 ID 或纯数据
- 缓存失效未触发:检查
revalidateTag与读取端是否声明tags - 第三方库依赖 DOM:在
use client文件中引用
生产部署清单
- 环境变量与密钥仅在 Server 端使用
- 对慢查询与外部 API 设定
timeout与降级占位 - 边缘与 Node 环境分开监控,错误率与耗时告警
- 构建后启用 Bundle 分析,限制客户端包体与重复依赖