一、为什么需要并发渲染
在 React 18 之前,一次 setState 会同步、不可中断地重渲染整棵受影响子树。当这次更新很重(例如对上千行表格做过滤),主线程被占满,用户输入、动画都会卡住。React 并发渲染(Concurrent Rendering)把渲染拆成可中断、可排优先级的任务:紧急更新(如键盘输入)插队到前面,非紧急更新(如重算列表)可以被暂停、复用或丢弃,从而让界面始终可交互。
并发渲染不是某个开关,而是一组 API:useTransition、useDeferredValue 和 Suspense。下面用可运行示例逐个讲清。
二、useTransition:把非紧急更新降级
useTransition 返回一个 startTransition 函数和 isPending 状态。把“重渲染”包进 startTransition,React 就把它标记为低优先级——如果期间来了紧急更新(输入框变化),低优先级任务会被中断重来,不会拖慢输入。
import { useState, useTransition, useMemo } from "react";
function SearchBox({ rows }) {
const [text, setText] = useState("");
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
// 重计算放在 useMemo,依赖 query 而非 text
const result = useMemo(
() => rows.filter(r => r.toLowerCase().includes(query.toLowerCase())),
[rows, query]
);
return (
<>
<input
value={text}
onChange={(e) => {
const v = e.target.value;
setText(v); // 紧急更新:输入框立刻响应
startTransition(() => setQuery(v)); // 非紧急:列表重算可被中断
}}
/>
{isPending ? <p>计算中...</p> : <Results data={result} />}
</>
);
}
关键点:setText(紧急)立刻改输入框,setQuery(非紧急)被放进 transition。哪怕 result 的过滤很慢,输入框也不会掉字;isPending 还能用来显示“计算中…”的友好提示。
三、useDeferredValue:延迟一个“慢”值
如果你只想“延迟某个值本身”而不想手动包一层 transition,用 useDeferredValue 更省事。它返回值的延迟副本:当前值立即更新(用于输入框),延迟值随后跟上(用于重渲染)。
import { useState, useDeferredValue, useMemo } from "react";
function DeferredSearch({ items }) {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text); // 延迟跟随的"慢值"
const results = useMemo(
() => items.filter(i => i.title.includes(deferredText)),
[items, deferredText]
);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* 用 deferredText 渲染,输入始终灵敏 */}
<ResultList text={deferredText} results={results} />
</>
);
}
它和 useTransition 本质同源:都是“让紧急的先走”。区别是 useTransition 包的是一段更新动作,useDeferredValue 直接产出可延迟的值,适合在子组件里消费。
四、Suspense 与并发数据加载
Suspense 在并发模式下表现更顺滑:被它包裹的懒加载组件在就绪前只显示 fallback,且不会阻塞页面其余部分的交互。配合 React.lazy 做代码分割,首屏更轻、交互更稳。
import { Suspense, lazy } from "react";
const HeavyChart = lazy(() => import("./HeavyChart")); // 代码分割,按需加载
function Dashboard() {
return (
<Suspense fallback={<Spinner />}>
{/* 并发模式下 fallback 仅遮住图表区,不阻塞页面其他输入 */}
<HeavyChart />
</Suspense>
);
}
五、三大特性的适用边界
| 特性 | 适合的场景 | 不适合的场景 |
|---|---|---|
| useTransition | 用户输入后触发的大面积状态更新(筛选、切 Tab、排序) | 必须同步落地的写操作(如提交订单) |
| useDeferredValue | 某值驱动重渲染,但希望输入先响应 | 值需要“立刻精确”(如实时校验红框) |
| Suspense | 组件/数据懒加载时的兜底 UI | 放在过粗的边界,导致整页白屏 |
六、实战:把卡顿表格改造成并发
下面是一个订单表的真实改造:点击表头按关键字过滤上千行,原本会卡顿,用 transition 包裹后排序操作依旧跟手。
function OrderTable({ orders, keyword }) {
const [isPending, startTransition] = useTransition();
const [view, setView] = useState(() =>
orders.filter(o => o.owner.includes(keyword))
);
function onChange(kw) {
// 把"重排整张表"包进 transition,点击表头依然跟手
startTransition(() => {
setView(orders.filter(o => o.owner.includes(kw)));
});
}
return (
<div>
{isPending && <span className="tip">刷新中...</span>}
<table>{/* 渲染 view 中的上千行 */}</table>
</div>
);
}
经验法则:凡是“用户操作后、界面要重算一大片”的地方,优先考虑 useTransition;凡是要把某个值喂给重渲染组件、又不想卡输入框的地方,用 useDeferredValue。
七、它和 React Server Components 是什么关系
很多人把并发渲染和 React Server Components(RSC) 混为一谈,其实它们是两层东西:RSC 解决“组件在哪执行、如何减少客户端 JS”;并发渲染解决“客户端更新时如何不卡”。二者互补——RSC 负责把首屏数据在服务端算好,并发特性负责交互时的流畅度,并不互相替代。
八、常见陷阱
1)把紧急更新也塞进 transition:结果就是输入也变慢,本末倒置。2)滥用导致“双份渲染”:transition 内的状态若和紧急状态耦合不清,会出现两次计算。3)Suspense 边界过粗:一个边界包住整页,加载时整页白屏,应下沉到具体懒加载区块。4)以为加了就自动快:并发只解决“调度优先级”,重计算本身该优化的(如虚拟列表、记忆化)仍要做。
九、上线前用性能数据验证
并发特性的收益要用真实指标说话,而不是“感觉更快”。建议结合 前端性能优化 一文里的 LCP、INP 指标,在输入高频场景下对比启用前后的交互延迟,确认 isPending 真正兜住了重渲染。也可参考 React 性能优化 Hooks 做记忆化配合。
十、小结
React 并发渲染把“渲染可中断、可排优先级”变成了日常可用的 API。记住三件套:useTransition 降级非紧急更新、useDeferredValue 延迟慢值、Suspense 提供不阻塞的兜底。把重交互的地方交给它们,再把底层计算和指标优化做扎实,你的页面就能从“卡一下”进化到“丝般顺滑”。
十一、并发渲染不是“自动批处理”
React 18 同时引入了自动批处理(Automatic Batching):同一个事件回调里的多次 setState 会自动合并成一次渲染。很多人把它和并发渲染混为一谈,其实两者目标不同。自动批处理解决“合并”,让更新更省;并发渲染解决“优先级与可中断”,让更新不卡。一个判断标准:如果某次更新“慢一点也行,但绝不能卡住输入”,它就是 transition 的候选;如果“必须立刻、精确反映”,就不要放进 transition。边界判断清楚了,并发特性才会真正发挥作用。
| 维度 | 自动批处理 | 并发渲染(transition) |
|---|---|---|
| 解决什么 | 合并多次 setState,减少渲染次数 | 给更新排优先级、可中断 |
| 是否影响卡顿 | 间接(渲染次数变少) | 直接(输入始终优先) |
| 使用方式 | 默认开启,无需改动 | 需手动 useTransition / useDeferredValue |
十二、落地 Checklist
1)先找出界面里“输入后大片重渲染”的交互点,比如长列表筛选、Tab 切换、大数据排序;2)把对应的状态更新包进 useTransition,并用 isPending 给出“刷新中”之类的反馈;3)对“直接喂给重渲染组件的值”改用 useDeferredValue;4)给懒加载区块单独套一层 Suspense,避免整页兜底白屏;5)用 Lighthouse 或真实设备的 Web Vitals 对比 INP 指标,确认收益真实存在,而不是“感觉更快”。
做到这五步,重交互页面就能稳定保持灵敏的输入响应,用户体验会有质的提升。并发渲染不是银弹,它只接管“更新如何被调度”,底层该做的虚拟列表、记忆化、数据裁剪依然不能少——把这两层叠加,页面才真正丝般顺滑。




