并发渲染是 React 18 的核心架构升级——渲染可以被中断、恢复、放弃,让紧急更新优先响应,非紧急更新延后处理。
一、什么是并发渲染
1.1 从”一口气跑完”到”跑一段歇一段”
React 18 之前,一旦开始渲染,就必须一口气跑完——中间不能停、不能断、不能让路。
React 18 引入了并发渲染(Concurrent Rendering):渲染过程可以被中断、恢复、甚至放弃。
类比:并发渲染像交通灯调度——紧急车辆(用户输入)来了,普通车辆(列表过滤)让路;紧急车辆过了,普通车辆继续走。
React 17 渲染时间线:
|--- 渲染(不可中断,50ms)---------------------------------------|
用户输入被阻塞 ❌ 动画掉帧 ❌
React 18 并发渲染时间线:
|-- 渲染片段 1 --| 处理用户输入 |-- 渲染片段 2 --| 渲染帧 |-- 片段 3 --|
✅ 立即响应 ✅ 不掉帧
1.2 并发不是多线程
并发渲染仍然是单线程的。React 没有引入 Web Worker 或多线程渲染,它做的只是更智能的任务调度——把渲染拆成小片段,遇到更紧急的任务就让路。
并发渲染的本质是协作式多任务——React 主动让出主线程,靠的是 Fiber 架构提供的可中断渲染能力。
1.3 并发渲染的三个能力
- 中断:渲染到一半可以停下来,让主线程处理更紧急的事
- 恢复:停下来之后,下次从断点继续,不用从头来
- 放弃:如果中途有了新的更新,旧的渲染可以直接丢弃
这三个能力不是新 API,而是底层行为的变化。你只需要用新 API(useTransition、useDeferredValue)告诉 React 哪些更新优先级低。
二、与 Fiber 的关系
2.1 Fiber 是基础设施,并发特性是上层 API
React渲染机制深入-Fiber与调度 中详细讲了 Fiber——React 16 引入的可中断渲染引擎。但 React 16/17 有了 Fiber,却没有真正”并发”——Fiber 只是具备了中断能力,默认行为仍然是同步渲染。
React 18 做的事情:把 Fiber 的中断能力真正用起来,并给开发者提供上层 API 控制优先级。
┌─────────────────────────────────────┐
│ 用户层 API │
│ useTransition / useDeferredValue │ ← React 18 新增
├─────────────────────────────────────┤
│ 并发渲染调度器 │
│ 优先级调度 / 时间切片 / 可中断渲染 │ ← React 18 新增
├─────────────────────────────────────┤
│ Fiber 架构 │
│ 链表遍历 / 双缓冲 / 可中断单元 │ ← React 16 已有
└─────────────────────────────────────┘
类比:Fiber 是修好的高速公路(路能走),React 18 是装了智能红绿灯(知道谁先走)。
2.2 为什么 React 16/17 没有并发
Fiber 是必要条件,但不是充分条件。并发渲染还需要:
- 优先级系统:区分哪些更新紧急、哪些不紧急
- 调度策略:高优先级插队、低优先级让路
- 防饥饿机制:低优先级不能永远等不到执行
- 一致性保证:中断恢复后,UI 状态不能半新半旧
这些 React 18 才完善。
三、Automatic Batching
3.1 什么是批量更新
多次 setState 调用,只触发一次重新渲染:
function handleClick() {
setCount(1); // 不会立即渲染
setFlag(true); // 不会立即渲染
setName("yi"); // 不会立即渲染
// 三次 setState 合并,只触发一次渲染 ✅
}
3.2 React 17 的局限:只在 React 事件中批量
React 17 的批量更新只在 React 事件处理器中生效。在 setTimeout、Promise、原生事件中,每次 setState 都触发一次渲染:
// ❌ React 17:setTimeout 中不批量,触发 3 次渲染
setTimeout(() => {
setCount(1); // 渲染 1
setFlag(true); // 渲染 2
setName("yi"); // 渲染 3
}, 1000);
原因:React 17 的批量更新依赖合成事件系统的包装,异步回调不在 React 控制范围内。
3.3 React 18:所有场景都自动批量
React 18 用并发渲染的调度机制替代了依赖合成事件的批量更新,不管在哪里调用 setState,都自动批量:
// ✅ React 18:所有场景都自动批量,只触发 1 次渲染
setTimeout(() => {
setCount(1);
setFlag(true);
setName("yi");
}, 1000);
| 场景 | React 17 | React 18 |
|---|---|---|
| React 事件处理器 | ✅ 批量 | ✅ 批量 |
| setTimeout | ❌ 不批量 | ✅ 批量 |
| Promise 回调 | ❌ 不批量 | ✅ 批量 |
| 原生 DOM 事件 | ❌ 不批量 | ✅ 批量 |
3.4 flushSync:强制同步刷新
有时确实需要”立即渲染”——比如 setState 后立刻读取 DOM 尺寸。flushSync 是 escape hatch:
import { flushSync } from "react-dom";
function handleClick() {
flushSync(() => {
setCount(1); // 立即渲染
});
// 此时 DOM 已更新,可以安全读取
const height = ref.current.offsetHeight;
}
⚠️ flushSync 会强制同步刷新整个组件树中待处理的更新,性能开销大,谨慎使用。
日常开发中,99% 的场景不需要
flushSync。自动批量就是最好的默认行为。
四、useTransition
4.1 问题:紧急更新被非紧急更新阻塞
经典场景:搜索框输入 + 列表过滤。
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
function handleChange(e) {
setQuery(e.target.value); // 更新输入框
setResults(filterItems(e.target.value)); // 更新列表(可能很慢)
}
// ...
}
问题:setQuery 和 setResults 是同等优先级。列表过滤耗时,输入框也被拖慢——用户打字感觉卡顿。
4.2 useTransition:标记低优先级更新
const [isPending, startTransition] = useTransition();
startTransition(fn):把fn中的状态更新标记为低优先级isPending:布尔值,是否有过渡更新正在进行
改写搜索场景:
function SearchPage() {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// 紧急更新:输入框立即响应
setQuery(e.target.value);
// 非紧急更新:列表过滤可以延后
startTransition(() => {
setResults(filterItems(e.target.value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList results={results} />
</>
);
}
类比:useTransition 像餐厅的”预订单”——你点了菜(状态更新),但告诉厨房”不急,先做别人的”(低优先级)。而正常下单(紧急更新)是”我现在就要”。
4.3 isPending 的用途
// 1. 显示加载指示器
{isPending && <Spinner />}
// 2. 降低列表视觉优先级
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<ResultList results={results} />
</div>
// 3. 禁用交互,避免过渡期间误操作
<button disabled={isPending}>提交</button>
4.4 startTransition 的注意事项
// ✅ 正确:在 startTransition 内调用 setState
startTransition(() => {
setResults(newResults);
});
// ✅ 更好:把耗时计算也放进 startTransition
startTransition(() => {
setResults(filterItems(query)); // 计算和更新都在低优先级
});
// ❌ 错误:不能在 startTransition 内使用 async/await
startTransition(async () => {
const data = await fetchData(); // ❌ 不支持异步
setResults(data);
});
// ✅ 正确:先获取数据,再标记过渡
const data = await fetchData();
startTransition(() => {
setResults(data);
});
startTransition只能包裹同步的状态更新。异步操作在await之后再标记为过渡。
五、useDeferredValue
5.1 延迟一个值的更新
const deferredValue = useDeferredValue(value);
当 value 变化时,deferredValue 不会立即更新——如果有更紧急的更新在排队,它会等紧急更新处理完再更新。
5.2 用 useDeferredValue 改写搜索场景
function SearchPage() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query); // 延迟版本的 query
return (
<>
{/* 输入框绑定原始 query,立即响应 */}
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* 列表使用延迟版本,不阻塞输入 */}
<ResultList query={deferredQuery} />
</>
);
}
// ResultList 用 React.memo 包裹,deferredQuery 没变时不重渲染
const ResultList = React.memo(function ResultList({ query }) {
const results = filterItems(query); // 可能耗时的过滤
return (
<ul>
{results.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
});
关键点:ResultList 用了 React.memo——当 deferredQuery 没变时不重渲染。配合模式:延迟值 + memo。
5.3 useDeferredValue vs useTransition
| useTransition | useDeferredValue | |
|---|---|---|
| 思路 | 主动标记:我告诉你这个更新优先级低 | 被动延迟:你帮我延迟这个值 |
| 控制粒度 | 控制整个更新(可包含多个 setState) | 控制单个值 |
| 有无 isPending | ✅ 有 | ❌ 没有,需自己判断 |
| 适用场景 | 你控制 setState 的调用 | 你只接收一个 prop/value |
| 代码侵入性 | 需修改状态更新逻辑 | 只需在消费端加一行 |
5.4 什么时候用哪个
// 场景 1:你控制 setState → 用 useTransition
startTransition(() => {
setResults(filterItems(query)); // 非紧急
});
// 场景 2:你只接收一个 prop → 用 useDeferredValue
const deferredQuery = useDeferredValue(query);
简单判断:能改 setState 的地方用 useTransition,只能改消费端的地方用 useDeferredValue。
六、Suspense 与并发渲染
6.1 并发模式下的 Suspense
Suspense机制详解 中详细讲了 Suspense 的机制。这里只说一点:Suspense 在并发模式下才能真正发挥威力。
React 17 的 Suspense 是”全有或全无”——组件加载中,整个区域显示 fallback。React 18 并发渲染让 Suspense 更智能:
- 不阻塞已有内容:已有数据的组件不会被新组件的加载阻塞
- 可中断的数据获取:用户快速切换时,旧请求可以被放弃
<Suspense fallback={<Loading />}>
<ProfileHeader /> {/* 已有数据,立即显示 */}
<UserPosts userId={id} /> {/* 正在加载,显示 fallback */}
</Suspense>
6.2 Selective Hydration
SSR 场景下,React 18 引入了 Selective Hydration(选择性注水):按优先级逐步注水——用户正在交互的部分先注水,其他后注水。
<Layout>
<Suspense fallback={<Skeleton />}>
<Comments /> {/* 用户正在交互 → 优先注水 */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<Sidebar /> {/* 暂时不需要 → 延后注水 */}
</Suspense>
</Layout>
类比:Selective Hydration 像酒店入住——前台先给你房卡(核心交互),其他权限慢慢开通。
七、实战:搜索框优化完整示例
7.1 优化前:输入卡顿
function App() {
const [query, setQuery] = useState("");
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* 10000 条数据过滤,每次输入都卡 */}
<SlowList query={query} />
</>
);
}
7.2 方案 A:useTransition
function App() {
const [inputValue, setInputValue] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const [isPending, startTransition] = useTransition();
return (
<>
<input
value={inputValue}
onChange={e => {
setInputValue(e.target.value); // 紧急:输入框立即更新
startTransition(() => {
setSearchQuery(e.target.value); // 非紧急:列表延后更新
});
}}
/>
{isPending && <Spinner />}
<SlowList query={searchQuery} />
</>
);
}
7.3 方案 B:useDeferredValue
function App() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SlowList query={deferredQuery} />
</>
);
}
const SlowList = React.memo(function SlowList({ query }) {
const items = heavyFilter(query);
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
7.4 两种方案的选择
| 方案 A(useTransition) | 方案 B(useDeferredValue) | |
|---|---|---|
| 代码量 | 稍多(两个 state) | 更少(一个 state + 一行 defer) |
| 过渡状态 | ✅ 有 isPending | ❌ 需自己比较 query !== deferredQuery |
| 控制力 | 更强(可包裹多个 setState) | 更弱(只能延迟一个值) |
| 推荐场景 | 需要过渡 UI 反馈 | 只需延迟渲染 |
大多数搜索场景,useDeferredValue 更简洁。需要过渡状态指示器时,用 useTransition。
八、常见误区
8.1 “并发渲染让我的 App 变快了”
❌ 并发渲染不会让单次渲染更快。它做的是让紧急更新不被非紧急更新阻塞——用户感知更流畅,但总工作量没变。
✅ 正确理解:并发渲染让 App 响应更快,不是渲染更快。
8.2 “useTransition 里可以做异步操作”
❌ startTransition 不支持 async 回调,只能包裹同步的 setState。
// ❌ 错误
startTransition(async () => {
const data = await fetchResults(query);
setResults(data);
});
// ✅ 正确
const data = await fetchResults(query);
startTransition(() => {
setResults(data);
});
8.3 “所有 setState 都应该放进 startTransition”
❌ 只有非紧急的、用户不期望立即看到结果的更新才应该放进 startTransition。输入框的值、开关状态、表单提交反馈——这些都是紧急的。
✅ 判断标准:用户是否期望这个更新立即生效? 期望 → 紧急;不期望 → 可标记为过渡。
8.4 “useDeferredValue 会丢数据”
❌ useDeferredValue 不会丢数据——它只是延迟更新。最终值一定会和原始值同步,只是中间可能短暂显示旧值。
✅ 延迟 ≠ 丢弃。就像慢一拍的回声——声音还在,只是晚了一点。
九、API 速查
useTransition
const [isPending, startTransition] = useTransition();
// isPending: boolean — 是否有过渡更新在进行
// startTransition: (callback: () => void) => void — 标记回调中的更新为低优先级
useDeferredValue
const deferredValue = useDeferredValue(value);
// value: 任意类型 — 需要延迟的值
// deferredValue: 与 value 同类型 — 延迟版本的值
// 配合 React.memo 使用效果最佳
flushSync
import { flushSync } from "react-dom";
flushSync(() => {
setState(newValue);
});
// 强制同步刷新,立即完成 DOM 更新
// ⚠️ 性能开销大,谨慎使用
十、总结
一句话:React 18 并发渲染 = Fiber 的中断能力 + 优先级调度 + 用户层 API(useTransition / useDeferredValue),让紧急更新不被非紧急更新拖累。
核心要点:
- 并发渲染:渲染可中断、可恢复、可放弃,仍是单线程
- Fiber 是基础设施,并发特性是上层 API——Fiber 提供能力,React 18 提供调度
- Automatic Batching:所有场景自动批量,
flushSync是 escape hatch - useTransition:主动标记低优先级更新,有 isPending 状态
- useDeferredValue:被动延迟值的更新,配合 memo 使用
- Suspense + 并发:已有内容不被新加载阻塞,Selective Hydration 按优先级注水
相关
- React渲染机制深入-Fiber与调度 — Fiber 架构的底层实现
- Suspense机制详解 — Suspense 的完整机制
- useCallback与useMemo — 另一类性能优化手段
- useState完全指南 — 状态管理基础