React 18 自动批处理(Automatic Batching)与 flushSync
一句话结论:React 18 默认会把同一“事件回合”(tick)里产生的多次更新合并渲染,以减少渲染次数;当你必须立刻把更新落到 DOM(例如与第三方库交互、读取布局)时,用
flushSync作为“强制同步提交”的逃生阀,但要慎用。
1. 它解决的问题是什么?
在 React 18 之前,只有 React 合成事件(onClick/onChange 等)里会批处理更新;而在 setTimeout / Promise / 原生事件 里,更新可能会多次渲染。
React 18 把批处理扩展为“默认更广泛的批处理”,让你在更多场景下:
- 更少的渲染次数(性能更好)
- 更一致的行为(心智负担更小)
2. React 18 的自动批处理:到底批到哪里?
2.1 典型现象(React 18)
- React 合成事件里多次
setState:批处理(一次 render) setTimeout回调里多次setState:也会批处理(一次 render)Promise.then/async/await之后的多次setState:也会批处理
直观记法:React 18 更倾向于“这一轮任务里触发的更新一起算”。
2.2 你需要理解的边界
- 批处理发生在 render/commit 的调度层面:多次更新会被合并成一次渲染提交
- 但并不是“永远只渲染一次”:
- 如果你跨了多个 tick(多个宏任务/多次事件触发),仍然会多次渲染
flushSync会打断批处理(见下文)
3. 为什么你会“感觉行为变了”?(项目常见坑)
坑 1:第三方库需要立刻读 DOM,但 DOM 还没更新
典型场景:
- 调用
setState后马上初始化/刷新一个第三方 UI(图表、富文本、地图) - 或者你需要在更新后立刻
getBoundingClientRect()读取布局
在 React 18 的自动批处理下,这个“马上”可能发生在 DOM commit 之前。
坑 2:依赖“渲染次数”的副作用(不推荐的写法)
例如把某些逻辑隐式地绑定到“每次 setState 都会触发一次渲染/一次 effect”。React 18 更强的批处理会让这种假设失效。
4. flushSync:什么时候用?怎么用?
flushSync 的作用:
- 让 React 立即处理并提交你包裹的更新
- 强行把更新从“可能批处理/延后 commit”变成“现在就 commit”
示例(伪代码):
import { flushSync } from 'react-dom'
function onOpen() {
flushSync(() => {
setOpen(true)
})
// 这里开始,DOM 已经提交(open=true 对应的节点已存在)
thirdPartyWidget.mount(document.querySelector('#panel')!)
}
使用建议(架构层面的约束)
- 优先解法:把“读 DOM / 调第三方库”的逻辑放到
useLayoutEffect(需要同步读布局)- 或
useEffect(不需要同步)
- 只在必要时使用 flushSync:
- 例如必须在同一调用栈内拿到最新 DOM
- 或外部系统的 API 约束无法改变
记住:
flushSync是“逃生阀”,不是常规工具。滥用会降低并发能力,影响响应性。
5. 与并发(Concurrent)/ Transition 的关系
- 自动批处理解决的是“同一轮更新少渲染”
- Transition 解决的是“让不紧急的更新可被打断/延后”,优先保证交互
flushSync则相反:它要求“立刻同步提交”,会压缩 React 的调度空间
因此在体验治理上通常是:
- 大型更新:考虑 Transition
- 必须立刻落 DOM:才用 flushSync
6. 自检清单(Checklist)
- 是否存在
setState后立刻读 DOM 的逻辑?能否迁移到useLayoutEffect? - 是否存在依赖“渲染次数/时序”的隐式逻辑?能否改成显式状态机/显式 effect?
- 是否因为第三方库被迫用
flushSync?是否能封装成适配层,避免扩散?
关联阅读
- 并发模式概念:
notes/03-concurrent-mode/01-concept.md - 时间切片:
notes/03-concurrent-mode/10-time-slicing.md - 面试题:setState 同步还是异步:
notes/08-interview/05-setState-sync-or-async.md