外观
React 18/19 并发渲染与 Server Components
React 18 带来的最大变化不是某个 API,而是渲染模型从"一次性同步完成"变成了"可中断、可调度"。React 19 则把这种模型延伸到服务端和表单交互。
理解并发渲染和 Server Components(RSC),关键是分清两件事:哪些工作可以在客户端被调度,哪些工作根本不需要发送到客户端。
并发渲染解决了什么
在 React 18 之前,一次 setState 会触发同步渲染:React 开始计算新的 UI,直到整棵树 diff 完成才把控制权交还浏览器。组件树很大时,这会阻塞输入和动画,用户感觉卡顿。
并发渲染允许 React:
- 中断渲染,先处理更高优先级的更新(例如用户输入)。
- 在后台准备新的 UI,准备好后再提交。
- 对同一状态产生多个版本的 UI,但只提交最终结果。
注意,并发渲染不等于"多线程渲染"。JavaScript 仍然是单线程,React 只是把渲染工作切成可中断的小块,在浏览器帧之间调度。
Transition:区分紧急与非紧急更新
用户输入、点击反馈是紧急更新,必须立即响应;列表过滤、图表重算可以延后。useTransition 用来标记非紧急更新:
tsx
import { useState, useTransition } from 'react'
export function SearchPanel() {
const [keyword, setKeyword] = useState('')
const [results, setResults] = useState<string[]>([])
const [isPending, startTransition] = useTransition()
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.target.value
setKeyword(value)
startTransition(() => {
setResults(search(value))
})
}
return (
<div>
<input value={keyword} onChange={handleChange} placeholder="搜索" />
{isPending && <span>搜索中...</span>}
<ResultList items={results} />
</div>
)
}输入框的更新是紧急的,列表更新被标记为 transition 后可以被中断。即使用户连续输入,React 也不会为了渲染旧结果而阻塞新输入。
useDeferredValue 适合"用一个延迟版本的值渲染非关键部分":
tsx
import { useDeferredValue, useMemo, useState } from 'react'
export function FilterableList({ items }: { items: string[] }) {
const [keyword, setKeyword] = useState('')
const deferredKeyword = useDeferredValue(keyword)
const filtered = useMemo(
() => items.filter((item) => item.includes(deferredKeyword)),
[items, deferredKeyword]
)
return (
<>
<input value={keyword} onChange={(event) => setKeyword(event.target.value)} />
<ul>
{filtered.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</>
)
}keyword 立即更新输入框,deferredKeyword 稍后更新列表。React 会在空闲时重新渲染列表,而不是阻塞输入。
自动批处理与 StrictMode
React 18 将自动批处理扩展到 Promise、setTimeout 和原生事件回调:
tsx
setTimeout(() => {
setCount((value) => value + 1)
setFlag((value) => !value)
// 只触发一次渲染
}, 0)开发环境下,StrictMode 会故意将 Effect 执行两次,用来暴露副作用清理不完整的问题。不要用 useRef 标记去"绕过"双执行,而应该确保 Effect 可重复执行:
tsx
useEffect(() => {
const controller = new AbortController()
loadData(controller.signal)
return () => controller.abort()
}, [])Server Components:把组件留在服务端
RSC 的核心思想是:不是所有组件都需要在浏览器运行。数据格式化、Markdown 渲染、权限判断、依赖大型库的组件,如果只在服务端执行,就可以完全不进入客户端 bundle。
在 Next.js App Router 中,组件默认是 Server Component:
tsx
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'
import { Comments } from './comments'
export default async function PostPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
if (!post) notFound()
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
<Comments postId={post.id} />
</article>
)
}Server Component 的特点:
- 可以是
async函数,直接await数据。 - 可以使用数据库、文件系统、服务端密钥。
- 不能使用
useState、useEffect、浏览器 API。 - 不向客户端发送组件代码,只发送渲染结果和客户端组件引用。
需要交互的组件通过 "use client" 标记:
tsx
'use client'
import { useState } from 'react'
export function Comments({ postId }: { postId: string }) {
const [content, setContent] = useState('')
async function submit() {
await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
})
setContent('')
}
return (
<div>
<textarea value={content} onChange={(event) => setContent(event.target.value)} />
<button onClick={submit}>发表评论</button>
</div>
)
}"use client" 标记的是模块边界,不是单个组件。被标记模块导入的所有组件都会进入客户端 bundle,所以应尽量把客户端组件放在树的叶子位置。
Server Actions
React 19 把表单提交和服务端变更整合得更自然。服务端定义 Action,客户端直接调用:
ts
// app/posts/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createComment(postId: string, formData: FormData) {
const content = String(formData.get('content') ?? '')
if (!content.trim()) {
return { error: '评论不能为空' }
}
await db.comment.create({ postId, content })
revalidatePath(`/posts/${postId}`)
return { error: null }
}客户端使用 useActionState 处理状态:
tsx
'use client'
import { useActionState } from 'react'
import { createComment } from '../actions'
export function CommentForm({ postId }: { postId: string }) {
const [state, formAction, isPending] = useActionState(
async (_previousState: { error: string | null }, formData: FormData) => {
return createComment(postId, formData)
},
{ error: null as string | null }
)
return (
<form action={formAction}>
<textarea name="content" required />
{state.error && <p role="alert">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '发表评论'}
</button>
</form>
)
}useOptimistic 可以先把预期结果渲染出来,失败后回滚,适合点赞、评论、购物车这类交互。
边界与数据流
RSC 架构有三条重要边界:
- 序列化边界:Server Component 传给 Client Component 的 props 必须可序列化,不能传普通函数、类实例、Symbol。
- 客户端边界:
"use client"以下的整棵子树都在客户端执行,边界要尽量下沉。 - 服务端边界:Server Action 和 Route Handler 是客户端访问服务端的入口,必须做鉴权和输入校验。
常见的数据流是:Server Component 取数 → 传给 Client Component 做交互 → 通过 Server Action 提交变更 → revalidatePath 刷新缓存。
React 19 与编译器
React 19 的几个变化值得关注:
- Actions:表单提交、异步状态和错误处理有统一模型。
use():在客户端读取 Promise 或 Context,配合 Suspense 使用。useOptimistic:声明式乐观更新。- React Compiler:自动分析组件依赖并插入缓存,减少手写
useMemo、useCallback和memo的需求。
编译器能降低性能优化的心智负担,但它不是"性能银弹"。数据结构、渲染边界和网络请求设计仍然决定上限。
常见坑
- 滥用 transition。所有更新都包
startTransition,会让紧急反馈也变慢。 - 客户端边界过大。在页面顶层加
"use client",整个页面失去 RSC 的收益。 - 在 Server Component 中使用浏览器 API。
window、localStorage、事件监听都不存在。 - 忘记 props 序列化限制。从服务端传函数给客户端会直接报错,应改为 Server Action 或事件契约。
- 缓存语义不清。Next.js 15 起
fetch默认不再缓存,需要显式声明cache或revalidate,升级时要逐页确认。 - hydration 不一致。服务端与客户端首次渲染结果不同,常见于时间、随机数和浏览器环境判断。
小结
并发渲染让客户端更新变得可调度,RSC 让一部分渲染工作根本不需要到达客户端。两者组合起来,React 应用的性能优化从"减少重渲染"扩展到了"减少发送到浏览器的代码和数据"。理解边界,比记住 API 更重要。