React useLayoutEffect vs useEffect:页面闪一下的元凶与正确修法你写了个 Tooltip,根据触发元素的位置动态算坐标。跑起来功能没问题,但每次弹出时它会先在左上角闪现一下,再跳到正确位置。这种肉眼可见的抖动,十有八九是因为你用了useEffect而不是useLayoutEffect。这两个 Hook 签名一模一样,90% 的场景可以互换,但剩下 10% 用错就会闪屏。这篇讲清它们的执行时机差异,以及什么时候必须换成useLayoutEffect。核心区别:在浏览器绘制之前还是之后React 更新 DOM 的大致流程是:执行组件函数 → 提交 DOM 变更 →浏览器绘制到屏幕→ 用户看到画面。两个 Hook 的差别就在于相对「绘制」这一步的时机:useEffect:在浏览器绘制之后异步执行。用户已经看到了这一帧,你的 effect 才跑。useLayoutEffect:在 DOM 变更后、浏览器绘制之前同步执行。它跑完,用户才看到画面。关键推论:如果你的 effect 里读取 DOM 布局(尺寸、位置)然后又修改 DOM,用useEffect会让用户先看到「修改前」的一帧、再看到「修改后」的一帧——这就是闪屏。而useLayoutEffect在绘制前就改完了,用户只看到最终结果。复现闪屏:一个定位 Tooltipimport { useEffect, useRef, useState } from react; function Tooltip({ targetRef, children }) { const tipRef useRef(null); const [pos, setPos] useState({ top: 0, left: 0 }); useEffect(() { // 读取触发元素的位置,算出 tooltip 该放哪 const rect targetRef.current.getBoundingClientRect(); setPos({ top: rect.bottom 8, left: rect.left }); // 问题:此时浏览器已经把 tooltip 画在了 {0,0}, // setPos 触发重渲染后才移到正确位置 —— 用户看到它跳了一下 }, [targetRef]); return ( div ref{tipRef} style{{ position: fixed, top: pos.top, left: pos.left }} {children} /div ); }初始pos是{0,0},浏览器先把 tooltip 绘制在左上角,useEffect绘制之后才跑、才 setPos、才移到正确位置。中间那一帧的左上角残影,就是用户看到的闪烁。修法:换成 useLayoutEffect只改一个词:import { useLayoutEffect, useRef, useState } from react; function Tooltip({ targetRef, children }) { const tipRef useRef(null); const [pos, setPos] useState({ top: 0, left: 0 }); useLayoutEffect(() { const rect targetRef.current.getBoundingClientRect(); setPos({ top: rect.bottom 8, left: rect.left }); // useLayoutEffect 在绘制前同步执行,setPos 引发的重渲染也在绘制前完成, // 浏览器一次性画出最终位置 —— 没有中间帧,不闪 }, [targetRef]); return ( div ref{tipRef} style{{ position: fixed, top: pos.top, left: pos.left }} {children} /div ); }React 会在useLayoutEffect里的 setState 之后、绘制之前完成重渲染,所以用户直接看到正确位置。这就是它存在的意义:同步测量 修改 DOM,消除中间帧。那什么时候该用哪个?判断很简单,问一句:这个 effect 会不会在绘制前读取布局并修改视觉结果?用useLayoutEffect的典型场景(都涉及「测量后立即改样式」):读取元素尺寸/位置来定位(Tooltip、Popover、下拉菜单)。根据内容高度做动画的初始值(比如手风琴展开)。滚动位置的同步恢复(避免先跳到顶部再跳回)。其余情况一律用useEffect,包括:// 数据请求 —— 和布局无关,用 useEffect useEffect(() { fetch(/api/data).then(/* ... */); }, []); // 订阅事件、埋点、定时器 —— 都用 useEffect useEffect(() { const id setInterval(tick, 1000); return () clearInterval(id); }, []);别滥用:useLayoutEffect 会阻塞绘制既然useLayoutEffect能防闪屏,为什么不全用它?因为它是同步、阻塞绘制的:浏览器必须等它(以及它引发的重渲染)跑完才能画下一帧。如果你在里面干重活(复杂计算、大量 DOM 操作),会直接拖慢首屏、造成卡顿。原则:默认用useEffect,只有出现闪屏、或明确需要绘制前测量 DOM 时,才换useLayoutEffect。不要「为了保险」全部用它。附带一个 SSR 警告如果你用 Next.js 等服务端渲染,useLayoutEffect在服务端会打印一条警告:「useLayoutEffect does nothing on the server」。因为服务端没有真实 DOM 可测量。处理方式:确认这段逻辑确实只需在客户端跑,可以用「客户端挂载后才渲染」的模式规避,或把纯副作用挪回useEffect。不要为了消警告无脑改回useEffect而让闪屏复活——先判断它到底需不需要绘制前执行。小结useEffect在浏览器绘制后异步执行,useLayoutEffect在绘制前同步执行。「读取 DOM 布局 → 修改视觉结果」的 effect 用useEffect会闪一帧,换useLayoutEffect消除中间帧。数据请求、订阅、定时器等和布局无关的副作用,一律用useEffect。useLayoutEffect阻塞绘制,别滥用;里面别干重活。SSR 下useLayoutEffect会告警,按「是否真需要绘制前执行」来决定处理方式,而不是无脑改回去。一句话记忆点:画面闪一下、元素跳到最终位置,先想想那个 effect 是不是该从 useEffect 换成 useLayoutEffect。