为什么你的React组件总在闪烁?为什么元素位置计算总是不准确?答案就藏在useLayoutEffect中!在此前已经学习过了useEffect,二者又有什么不同呢?
useEffect 与 useLayoutEffect 的核心区别:执行时机
两者都是处理副作用的钩子,核心差异在于执行时机不同,这直接导致了它们在实际使用中的效果差异。
useEffect 的执行时机
useEffect
的副作用会在组件渲染完成后(DOM 更新并已绘制到屏幕) 执行,属于 "异步" 执行,不会阻塞浏览器的渲染流程。
JSX
useEffect(() => {
// 此时DOM已更新并显示在屏幕上
console.log('useEffect:渲染完成后执行');
}, []);
useLayoutEffect 的执行时机
useLayoutEffect
的副作用会在DOM 更新后、屏幕渲染前执行,属于 "同步" 执行,会阻塞渲染流程。
JSX
useLayoutEffect(() => {
// 此时DOM已更新,但尚未显示在屏幕上
console.log('useLayoutEffect:DOM更新后,渲染前执行');
}, []);
直观对比:执行顺序
jsx
function App() {
const ref = useRef();
useEffect(() => {
console.log('useEffect:', ref.current.offsetHeight);
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect:', ref.current.offsetHeight);
}, []);
return <div ref={ref} style={{ height: 100 }}></div>;
}
关键差异 :
useLayoutEffect
的代码会在用户看到新 UI 之前执行,而useEffect
的代码在用户看到新 UI 之后执行。
useLayoutEffect 能解决什么问题?核心价值
useLayoutEffect
的 "同步阻塞渲染" 特性,使其特别适合解决因 DOM 操作延迟导致的 UI 闪烁或抖动问题。
案例:修改文本内容和容器高度
jsx
function TextBox() {
const [content, setContent] = useState('初始长文本...');
const ref = useRef();
useEffect(() => {
// 1. 修改文本内容
setContent('更新后的文本内容...');
// 2. 同步修改容器高度
ref.current.style.height = '200px';
}, []);
return (
<div
ref={ref}
style={{ height: '100px', background: 'lightblue' }}
>
{content}
</div>
);
}
在刷新的时候,我们可以明显看到页面的闪烁,那么
为什么 useEffect 会闪烁?
- 初始渲染:显示 "初始长文本",高度 100px(已绘制到屏幕);
useEffect
执行:修改文本和高度为 200px;- 再次渲染:更新为最终状态。
用户会看到 "初始文本 + 100px 高度" 瞬间切换到 "新文本 + 200px 高度",出现闪烁。
而当把useEffect换成useLayoutEffect时 ,流程会变成:
- 初始渲染计算 DOM(未绘制);
useLayoutEffect
执行:修改文本和高度(DOM 更新);- 一次性绘制最终状态到屏幕。
用户直接看到最终结果,无中间状态,避免闪烁。
同步获取 DOM 信息:确保拿到最新状态
在需要获取 DOM 元素的样式、尺寸(如offsetHeight
、scrollTop
)时,useLayoutEffect
能保证拿到的是 "更新后、未渲染" 的最新 DOM 状态,而useEffect
可能因异步执行导致获取到旧状态(极端场景下)。 案例:弹窗居中定位
jsx
function Modal() {
const ref = useRef();
useLayoutEffect(() => {
// 获取弹窗高度和窗口高度,计算居中位置
const modalHeight = ref.current.offsetHeight;
const windowHeight = window.innerHeight;
// 同步设置marginTop,确保渲染时已居中
ref.current.style.marginTop = `${(windowHeight - modalHeight) / 2}px`;
}, []);
return (
<div
ref={ref}
style={{
position: 'absolute',
width: '200px',
height: '150px',
background: 'red'
}}
>
居中弹窗
</div>
);
}

为什么用 useLayoutEffect?
弹窗需要基于自身高度计算居中位置,useLayoutEffect
在 DOM 更新后立即执行,能同步拿到准确的offsetHeight
,并在渲染前设置好位置,确保弹窗一出现就是居中的。
若用useEffect
,可能在获取高度时弹窗尚未完成 DOM 更新,导致计算错误,出现定位偏差(后续再修正,产生抖动)。
useLayoutEffect 的使用注意事项
(1)避免耗时操作:会阻塞渲染
useLayoutEffect
在渲染前执行,且是同步阻塞的。如果在其中写耗时逻辑(如复杂循环、大量计算),会延迟页面渲染,导致用户看到 "白屏" 或 "卡顿"。
jsx
useLayoutEffect(() => {
// 耗时操作:阻塞渲染
for (let i = 0; i < 100000000; i++) {}
}, []);
(2)依赖项与执行时机
和useEffect
一样,useLayoutEffect
的第二个参数是依赖项数组:
- 空数组
[]
:仅在组件挂载后执行一次; - 依赖项变化:在 DOM 更新后、渲染前执行;
- 无依赖项:每次渲染后(DOM 更新后)都执行。
(3)与 useEffect 的选择原则
-
优先用
useEffect
:大多数场景下,异步执行不阻塞渲染,性能更好; -
用
useLayoutEffect
的场景:- 需要修改 DOM 以避免 UI 闪烁(如上述文本 + 高度修改案例);
- 需要同步获取 DOM 信息并立即应用(如弹窗居中、滚动位置调整)。
核心区别总结
特性 | useEffect | useLayoutEffect |
---|---|---|
执行时机 | 渲染完成后(已绘制到屏幕) | DOM 更新后、渲染前(未绘制) |
是否阻塞渲染 | 不阻塞(异步) | 阻塞(同步) |
适用场景 | 大多数副作用(数据请求、事件监听) | 处理 DOM 避免闪烁、同步获取 DOM 信息 |
性能影响 | 低(不阻塞) | 高(耗时操作会卡顿) |