它和 useEffect 有什么区别?
useEffect 是大多数情况下处理副作用的默认选择,它在浏览器完成布局与绘制之后异步执行,不会阻塞页面渲染。而 useLayoutEffect 会在 DOM 更新完成后、浏览器绘制之前同步执行,它会阻塞浏览器的绘制过程,直到副作用代码运行结束。
两者的调用时机可以用一条时间线清晰对比:
渲染触发 → React 更新 DOM → 浏览器计算布局
↓
useLayoutEffect 执行(同步,此时 DOM 已更新但屏幕未刷新)
↓
浏览器绘制屏幕(用户看到新 UI)
↓
useEffect 执行(异步,不阻塞绘制)
正是这一时机的差异,决定了它们的适用场景完全不同。
什么时候应该使用 useLayoutEffect?
useLayoutEffect 适合那些需要在浏览器绘制前同步读取或修改 DOM,以避免用户看到闪烁或不一致界面的场景。最常见的需求包括:
- 测量 DOM 节点的尺寸或位置(如获取元素高度、滚动位置),然后根据测量结果同步调整界面。
- 根据 DOM 状态立即修改样式,避免先渲染出一个错误的样式再迅速修正,造成视觉抖动。
- 动画或过渡前的必要计算,让最终渲染的界面就是动画起始的正确状态。
说白了,如果你要根据某个 DOM 的真实尺寸或位置去更新 UI,而这些更新又会影响本次渲染的画面,那么就应该使用 useLayoutEffect,确保计算和修改在画笔落下前完成。
实际例子:修复闪烁的 Tooltip 定位
假设你要实现一个 Tooltip,它的位置需要根据触发元素的位置动态调整,确保不超出屏幕边界。如果使用 useEffect,Tooltip 会先出现在默认位置(比如元素正下方),再进行位置修正,用户会看到明显的“闪现一下”。
function Tooltip({ targetRef, children }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
if (!targetRef.current || !tooltipRef.current) return;
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// 根据目标元素和 Tooltip 的实际尺寸计算最终位置
let top = targetRect.bottom + 8;
let left = targetRect.left;
// 避免右侧溢出
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 8;
}
setPosition({ top, left });
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
>
{children}
</div>
);
}
由于 useLayoutEffect 在浏览器绘制前执行,Tooltip 会以正确的最终位置直接出现在屏幕上,不会有闪烁。如果换成 useEffect,用户会先看到 Tooltip 出现在错误位置(比如默认左上角),而后跳到正确位置。
使用时的注意事项
- 尽量不要在里面执行耗时操作:因为
useLayoutEffect会阻塞绘制,内部如果有复杂计算或长任务,会导致页面卡顿感明显,表现为点击后界面“愣一下”才反应。这些耗时逻辑应该移到useEffect或 Web Worker 中处理。
- 大多数情况下不需要它:React 官方建议优先使用
useEffect,只在确实出现视觉闪烁或需要同步读取布局信息时才切换到useLayoutEffect。过早优化或过度使用反而会损害性能。
- 服务端渲染(SSR)中没有意义:
useLayoutEffect在服务端不会执行,而且在 SSR 阶段也无法访问真实的 DOM 布局。如果组件同时用于服务端和客户端,通常需要结合环境判断或退回到useEffect,并接受可能的一次闪烁。
- 依赖数组与 useEffect 完全一致:同样需要正确声明依赖,避免遗漏造成闭包陷阱。清理函数的写法和执行时机也与
useEffect一致(在组件卸载或依赖变化重新执行前运行)。
与 useEffect 的选用诀窍
一个简单有效的判断法则:如果副作用需要读取 DOM 布局信息,并且该信息会立刻影响这一次渲染的画面,使用 useLayoutEffect;否则一律使用 useEffect。当你发现部署后界面有微小闪烁时,再考虑将它改为 useLayoutEffect 即可。这个习惯可以帮你避免大部分性能陷阱。