一句话定义
并发渲染(React 18 起默认)让 React 可以中断渲染、按优先级排队、丢弃过时结果——不是「多线程」,而是把渲染变成可抢占的任务流,让紧急更新(输入)不被缓慢更新(大数据列表)阻塞。
为什么重要
它是理解 React 18/19 所有新 API 的钥匙:useTransition、useDeferredValue、Suspense 的流式行为、Actions 的语义,全部建立在「渲染可中断」之上。不懂调度,这些 API 只能当语法糖背下来。
前置知识
核心概念
- Lane(优先级模型):React 内部用位掩码给更新分优先级:同步输入 > 过渡(Transition)> 离屏(Offscreen)。
- 时间切片(Time Slicing):长渲染拆成 ≤5ms 的工作单元,之间让路给输入事件。
- 可中断渲染:渲染阶段随时暂停;期间来了高优先级更新 → 当前渲染丢弃重来(这就是组件必须纯的终极原因)。
- 过渡更新(Transition):被标记为「非紧急」的更新,可以让位于输入,且保证最终显示(不丢)。
- 并发安全的 API 时代:useDeferredValue、useSyncExternalStore、Suspense 流式、Actions。
原理 / 机制
同步模式 vs 并发模式的时序对比:
text【同步模式(React 17 及以前)】
输入事件 → 渲染整个慢列表(300ms) → 期间输入无响应 → 提交 → 下一帧
【并发模式(React 18+)】
输入事件 → 开始渲染慢列表(过渡优先级)
↘ 5ms 后用户又敲了一个字符
→ 高优先级更新抢占,慢渲染作废
→ 快速渲染新输入 → 提交
→ 空闲时重新以新状态续渲染慢列表
关键洞察:并发模式里同一个界面可能同时存在「已显示的旧 UI」和「渲染中的新 UI」。React 保证随时可退回旧 UI(已提交的那份),这要求:
- 组件纯函数(同一输入永远同一输出,重放无副作用)。
- 副作用全部挪到提交之后(effect)或事件中。
- 不在渲染期间读「会变的外部可变量」——外部 store 要用
useSyncExternalStore接入。
调度示意(简化):
jsxsetInput(e.target.value); // 紧急:立即渲染输入框
startTransition(() => {
setResults(filter(input)); // 过渡:慢,可被打断
});
直观类比
旧渲染像单一窗口的银行柜台:柜员办一笔大业务(慢渲染),后面排队的客户(输入事件)全部干等。并发渲染像叫号系统:VIP(输入)插队,柜员手头的业务单(正在计算的渲染)可以放下作废,办完 VIP 回来重开一张新单(以最新状态重放渲染);「过渡业务」是「可以慢慢办但绝不能弄丢」的排队任务。
实例 / 案例
可感知的并发最小 demo:
jsxfunction SearchPage() {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
function onChange(e) {
setInput(e.target.value); // 紧急:每个字符立即回显
startTransition(() => {
setQuery(e.target.value); // 过渡:搜索结果慢慢来
});
}
return (
<>
<input value={input} onChange={onChange} />
<Results query={query} /> {/* 内部可配合 Suspense */}
</>
);
}
体验差异:不加 transition 时,长列表页面每敲一键都整树重渲染、输入框都卡;加上后输入框丝滑,结果区延后刷新。细节 API(useTransition 返回 isPending、useDeferredValue)在 kp-023 展开。
并发时代的额外纪律(写法上的新要求):
jsx// 渲染期间读外部可变值(时间/随机/store)在并发下会"渲染两次得到不同结果"
const id = Math.random(); // ❌ 并发下两次渲染 id 不同 → 不一致 UI
const [id] = useState(() => Math.random()); // ✅ 快照进 state
// 订阅外部 store 的正确姿势
const online = useSyncExternalStore(subscribe, () => navigator.onLine);
常见误区
- 「并发 = 多线程 / 真并行」:都在主线程,靠让路与抢占实现「并发地安排工作」。
- 把 startTransition 包住 setState 就会更快:它不减少工作量,只降优先级——总时间可能不变,但交互不再被阻塞;真正提速仍靠 kp-019。
- 以为过渡更新可能被永久丢弃:不会——被抢占后会以最新状态重放,最终一定呈现。
- 在渲染中读 Date.now()/随机数:并发可重入使两次渲染结果不一致;需快照进 state 或 ref 初始化。
- 以为必须手动「开启并发模式」:React 18+ 使用任何并发 API(useTransition/Suspense 数据流)即按并发语义运行,不存在单独的「模式开关」(createRoot 只是新根 API)。
自测题
- 「并发渲染不是多线程」——那它靠什么实现「输入不被慢渲染卡住」?
- 为什么「渲染可中断」要求组件纯函数?举一个副作用被重复执行的例子。
- startTransition 包裹的更新会被丢弃吗?完整描述被抢占后的重放过程。
Math.random()写在渲染体里在并发下会出什么问题?两种正确写法。- useSyncExternalStore 解决的问题是什么?
与其他知识点的关系
- 向前:kp-008 的 Fiber 是它的物理基础,kp-020 的 Suspense 是它的声明面。
- 向后:kp-023 是它的两个用户级 API;kp-025 的 Actions 内部使用过渡优先级;kp-019 的优化手段与它正交互补(少渲染 vs 错峰渲染)。
延伸阅读
- React 18 发布文(并发特性总纲):<https://react.dev/blog/2022/03/29/react-v18>
- useTransition:<https://react.dev/reference/react/useTransition>
- useSyncExternalStore:<https://react.dev/reference/react/useSyncExternalStore>