news 2026/8/11 23:04:37

React 用 flushSync 强制同步刷新 DOM:自动滚到底部、读取最新布局与它的性能代价

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 用 flushSync 强制同步刷新 DOM:自动滚到底部、读取最新布局与它的性能代价

React 用 flushSync 强制同步刷新 DOM:自动滚到底部、读取最新布局与它的性能代价

聊天窗口来了新消息,你想让它自动滚到底部;或者点一下「展开」,你想马上量一下展开后的高度做动画。你写了setState之后紧接着操作 DOM,结果发现——量到的是旧的DOM,滚动也差了一屏。这篇讲清楚为什么会这样,以及flushSync什么时候是解药、什么时候是毒药。

先复现这个坑

一个最常见的场景:聊天列表,发消息后要滚到底。

function Chat() { const [messages, setMessages] = useState([]); const listRef = useRef(null); function send(text) { setMessages((prev) => [...prev, text]); // 想在新消息渲染后滚到底 —— 但这里 DOM 还没更新! const el = listRef.current; el.scrollTop = el.scrollHeight; // scrollHeight 还是加新消息之前的高度 } return ( <div ref={listRef} style={{ height: 300, overflow: "auto" }}> {messages.map((m, i) => ( <p key={i}>{m}</p> ))} </div> ); }

现象:每次发消息,列表滚到的是上一条消息的位置,永远差最后一条那么高。

原因是 React 18 的setState异步批处理的。调用setMessages只是排了个更新任务,函数继续往下执行时,组件还没重渲染,DOM 里还没有新的<p>,所以scrollHeight读到的是旧值。

朴素修法:useEffect,但它有闪烁

标准答案是把「读 DOM / 操作 DOM」放到useEffect里,等 React 提交完再执行:

useEffect(() => { const el = listRef.current; el.scrollTop = el.scrollHeight; }, [messages]); // messages 变了、DOM 更新后才跑

大多数场景这就够了,应该优先用它。但useEffect在浏览器绘制之后才执行,如果你的操作会改变布局(比如滚动位置、插入元素撑高),用户可能会看到「先渲染在错误位置、再跳一下」的闪烁。

想消灭闪烁可以换useLayoutEffect(绘制前同步执行)。绝大多数「渲染后滚动/量尺寸」用它就对了。那flushSync是干嘛的?

flushSync 登场:强制立即提交

flushSync来自react-dom,它包住一段setState,强制 React 同步地、立刻完成这次更新并提交到 DOM,函数返回后 DOM 已是最新:

import { flushSync } from "react-dom"; function send(text) { flushSync(() => { setMessages((prev) => [...prev, text]); }); // 到这行时,新的 <p> 已经在 DOM 里了 const el = listRef.current; el.scrollTop = el.scrollHeight; // 现在 scrollHeight 是最新高度,滚动正确 }

关键点:flushSync(fn)里的fn执行完,React 会立即冲刷这批更新、跑完 DOM diff 和提交,然后flushSync才返回。所以紧跟其后的命令式 DOM 读写拿到的都是最新状态。

真正非它不可的场景:一次操作里要「读旧 + 写新 + 读新」

有个场景useEffect很难优雅处理:在同一个事件里,既要拿更新前的布局,又要拿更新后的布局。比如往列表顶部插入内容,想保持用户当前看到的位置不跳(锚定滚动):

function prepend(item) { const el = listRef.current; const prevHeight = el.scrollHeight; // 1. 先记住旧高度 flushSync(() => { setMessages((prev) => [item, ...prev]); // 2. 同步插入并提交 }); // 3. 新内容撑高了顶部,把 scrollTop 补上高度差,视口锚定不动 el.scrollTop += el.scrollHeight - prevHeight; }

这种「测量 → 同步改 DOM → 再测量 → 补偿」的流程,用useEffect要靠 ref 在多次渲染间传值,又绕又容易错;flushSync把它压进一个同步函数里,直白得多。

性能代价:为什么官方叫你「少用」

flushSync是把 React 18 辛苦攒起来的批处理优化直接关掉。正常情况下,一个事件里的多次setState会被合并成一次渲染;而每个flushSync都会强制单独渲染并提交一次

function bad() { // 三次 flushSync = 三次完整的渲染 + 提交,浏览器被迫同步布局三遍 flushSync(() => setA(1)); flushSync(() => setB(2)); flushSync(() => setC(3)); } function ok() { // 需要同步的话,合并成一次 flushSync(() => { setA(1); setB(2); setC(3); }); }

代价具体是:

  • 打断批处理:本可合并的更新被拆成多次渲染,组件树越大越明显。
  • 强制同步布局(layout thrashing):提交后浏览器要立刻重算布局,循环里连续调用会造成布局抖动。
  • 让并发特性失效:它是同步的,和useTransitionSuspense的可中断渲染理念相悖,包在里面的更新无法被打断或降级。

React 官方文档对它的原话是「uncommon(不常见)」「can hurt performance(会损害性能)」。默认不要用

决策清单:到底该用哪个

  • 渲染后要滚动/量尺寸,允许一帧闪烁 →useEffect
  • 渲染后要滚动/量尺寸,不能闪烁(最常见) →useLayoutEffect
  • 必须在同一个同步函数里「改 state 后立刻读到最新 DOM」,尤其要做「旧布局 → 新布局」的补偿计算 →flushSync,且尽量只调一次、把多个setState合并进去。
  • 整页/大列表更新且想保持响应 → 用useTransition,别用flushSync

还有个隐藏坑:别在渲染过程中或useEffect内部调flushSync,React 会警告 “flushSync was called from inside a lifecycle method”。它只该出现在事件处理器这类命令式代码里。

小结

  • setState是异步批处理的,调用后紧接着读 DOM 拿到的是DOM,这是「滚动差一屏」的根因。
  • 优先用useLayoutEffect(绘制前、无闪烁)或useEffect来做「渲染后操作 DOM」。
  • flushSync强制同步提交,让你在一个函数内立刻读到最新 DOM,专治「读旧布局 → 改 → 读新布局 → 补偿」这类命令式流程。
  • 它的代价是关掉批处理、强制同步布局、让并发特性失效,所以要少用、合并用、只在事件里用。

一句话记忆:flushSync是「立刻兑现」的按钮,方便但贵——非要在同一函数里读到刚改完的 DOM 才按它,其余时候交给 Effect。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/11 23:00:58

从航空航天到工业自动化:LVDT传感器为何备受青睐?

LVDT诞生于二十世纪四十年代&#xff0c;最初是为航空工业量身定制的。八十年过去&#xff0c;它不仅没有被更新的技术替代&#xff0c;反而从航空航天扩展到了工业自动化、医疗器械、核能等几乎所有精密测量领域。 这种跨越时代的生命力&#xff0c;背后有清晰的逻辑。航空航天…

作者头像 李华
网站建设 2026/8/11 22:51:56

vLLM 推理优化实践

本地推理编排启动三个 vLLM 实例&#xff0c;共享一张 RTX 4070 的 8GB 显存&#xff1b;Qwen3-1.7B 模型经 FP8 量化后仍约占 ~4.5GB。高并发下 GPU 显存占用持续打满&#xff0c;P95 延迟尖刺明显&#xff0c;首 token 延迟抖动超过 2 秒。加卡预算充足&#xff0c;但本地开发…

作者头像 李华
网站建设 2026/8/11 22:44:38

【射频】电动窗帘遥控器PCB板载天线分析

一、背景 最近&#xff0c;家里边的电动窗帘的遥控器&#xff0c;常时间不使用&#xff0c;经常使用“小爱同学”&#xff0c;忽略了它。其结果是&#xff1a;电池漏液&#xff0c;彻底损坏。 今天到单位&#xff0c;迅速拆解看一眼。发现最上方的板载PCB天线上&#xff0c;打…

作者头像 李华