useEffect vs useLayoutEffect
副作用应该什么时候执行?什么时候需要阻塞浏览器绘制?
先记住选择原则:默认使用 useEffect。只有必须在浏览器绘制前读取或修改布局,并且不能接受用户看到中间状态时,才使用 useLayoutEffect。
1. 两者解决的问题不同
两者都在组件提交更新后运行,并且都可以返回 cleanup;区别在于它们相对于浏览器绘制的时机不同。
useEffect
useEffect 适合把 React 与外部系统同步,例如:
- 请求数据或同步网络状态;
- 订阅事件、WebSocket、定时器;
- 发送埋点;
- 调用不影响首屏布局的第三方 API。
它在 Commit 后由 React 调度执行,通常会让浏览器先完成绘制。但“绘制后执行”不是绝对的:如果更新由用户交互触发,React 可能会在浏览器绘制前运行某些 effect,让事件系统或外部观察者能看到 effect 的结果。
所以业务代码不要依赖“useEffect 一定在 paint 后”这种过于绝对的时间顺序。只要逻辑必须在用户看到页面前完成,就不应该交给 useEffect 赌调度时机,而应该考虑 useLayoutEffect 或重新设计布局方案。
useLayoutEffect
useLayoutEffect 会在 DOM 更新后、浏览器绘制前同步执行。它适合:
- 测量 DOM 尺寸或位置;
- 根据测量结果同步调整布局;
- 初始化必须在绘制前完成的 DOM 第三方库;
- 避免用户看到明显的布局跳动。
它会阻塞浏览器绘制,因此回调必须短小,不能在里面执行昂贵计算、网络请求或长时间循环。
如果在 useLayoutEffect 中调用 setState,React 会在浏览器绘制前同步处理这次更新,并继续执行剩余的 layout effect。这样可以避免用户看到错误位置或中间状态,但代价是首帧绘制会被进一步推迟。
2. 执行顺序
一次更新可以简化理解为:
useLayoutEffect 不是在 render 前执行,也不是 React 真的监听到了浏览器的 paint 时刻。它能发生在绘制前,是因为 React 把它放进了同步的 Commit 流程中。
浏览器绘制需要等待当前 JavaScript 任务结束。一次提交中,React 会先把变更写入真实 DOM,然后立刻同步执行 layout effect;如果 layout effect 里触发了状态更新,React 还会在绘制前继续处理这次更新。等这段同步工作全部结束后,浏览器才有机会进行 layout、paint。
所以可以这样理解:
当依赖变化时,React 会先清理旧 effect,再执行新的 setup:
这个顺序是面试时的简化模型。更准确地说,同一种 effect 重新执行前会先清理上一次的 effect;layout effect 属于同步的提交阶段,会在浏览器绘制前处理;passive effect(也就是 useEffect)由 React 之后调度,具体时机可能受更新来源和调度策略影响。
可以稳定依赖的原则是:每个 effect 的 cleanup 应撤销它自己的订阅、定时器或 DOM 外部操作,不要依赖不同组件、不同 effect 之间过于细粒度的全局执行顺序。
3. DOM 测量示例
假设要根据 tooltip 自身尺寸设置位置:
如果使用 useEffect,浏览器可能先绘制初始位置,再执行测量和更新,用户可能看到一次闪烁。useLayoutEffect 可以在绘制前完成这次修正。
但更好的方案通常是优先使用 CSS 布局、CSS Anchor Positioning 或成熟的定位库;只有确实需要 JavaScript 测量时,才引入布局 effect。
真实项目还要考虑尺寸变化:窗口 resize、字体加载、内容异步变化、容器滚动都可能让初次测量失效。这时通常需要配合 ResizeObserver、滚动监听、重新测量逻辑,或直接使用 Floating UI、Popper 这类定位库。
4. 哪些场景不该用 useLayoutEffect?
以下场景通常使用 useEffect 即可:
不要仅因为“想让代码更快”就使用 useLayoutEffect。它不会让网络请求更快,也不会减少渲染次数,反而可能延迟绘制,造成卡顿。
一个常见误区是:认为 useLayoutEffect 比 useEffect “更早执行,所以性能更好”。实际恰好相反,它把工作塞进浏览器绘制前的关键路径中。如果里面读取布局后又写样式,或者连续触发布局读取和写入,还可能造成 forced reflow,使页面更卡。
下面这些行为尤其不应该放在 layout effect 中:
- 数据请求;
- 日志和埋点;
- 大量计算;
- 可以通过 CSS 完成的样式设置;
- 与布局无关的订阅。
5. useInsertionEffect 是什么?
useInsertionEffect 的执行时机比 useLayoutEffect 更早,主要为 CSS-in-JS 库插入样式设计。普通业务组件不应使用它:
- 它不能访问已经更新的 DOM;
- 不适合读取布局;
- 不适合替代
useEffect或useLayoutEffect。
可以这样记:
6. SSR 注意事项
服务端没有真实 DOM,也没有浏览器布局,因此 effect 不会在服务端执行。useLayoutEffect 在服务端渲染时通常会产生警告,因为它要求在绘制前执行,但服务端不存在这个阶段。
仅在回调内部判断 window 不能消除这个警告:
常见处理方式是让依赖布局的组件只在客户端渲染,或在明确需要同构支持时使用封装:
使用这种封装时仍要确认:服务端输出和客户端首次渲染必须保持一致,避免 hydration mismatch。
在 Next.js 等 SSR 框架中,如果组件本身强依赖真实 DOM 尺寸,通常可以把这部分拆成 client-only 组件,或先渲染一个服务端和客户端一致的占位状态,等客户端挂载后再测量和修正。
7. Strict Mode 与 cleanup
开发环境 Strict Mode 可能额外执行一次 setup → cleanup → setup,用来发现副作用是否可重复初始化、是否正确清理。这不是 useLayoutEffect 或 useEffect 的异常行为。
无论使用哪一种 effect,都应该让 setup 和 cleanup 成对出现。不要依赖“只执行一次”的假设,也不要在 cleanup 中执行与旧资源无关的新业务操作。
8. 选择流程
可以按下面的顺序判断:
- 这段逻辑是否在与外部系统同步?如果不是,先确认是否根本不需要 effect。
- 如果需要 effect,是否必须在绘制前读取或修改布局?
- 如果不需要,使用
useEffect。 - 如果需要,使用
useLayoutEffect,并尽量缩短同步工作。 - 如果只是 CSS-in-JS 样式注入,才考虑由库使用
useInsertionEffect。
9. 面试深挖点
为什么 useEffect 不能解决闪烁?
因为 useEffect 通常不会阻塞浏览器绘制。组件先按初始状态提交到 DOM,浏览器可能已经把这个中间状态画出来,然后 effect 才测量 DOM、更新位置,用户就会看到一次跳动。
useLayoutEffect 的区别不是“更快”,而是“更早且同步”:它能在绘制前读取布局并触发修正更新,让用户只看到修正后的结果。
为什么不能全部换成 useLayoutEffect?
因为它会阻塞绘制。浏览器必须等 layout effect 执行完,甚至等其中触发的同步更新完成后,才能把页面画出来。少量 DOM 测量可以接受,网络请求、日志、订阅、大计算放进去就会拖慢首帧和交互响应。
它们和类组件生命周期怎么类比?
可以粗略类比:
useLayoutEffect接近componentDidMount/componentDidUpdate中同步读取 DOM、调整布局的场景;useEffect更适合componentDidMount/componentDidUpdate中异步订阅、请求、埋点等不阻塞绘制的场景。
但 Hook 不应该机械等同于生命周期。effect 描述的是“这次渲染后如何与外部系统同步”,依赖数组决定它响应哪些渲染数据,而不是单纯模拟 mount/update。
总结
面试时可以这样回答:默认使用 useEffect 与外部系统同步;如果必须在浏览器绘制前测量或修正布局,使用 useLayoutEffect。useLayoutEffect 中触发的更新会在绘制前同步处理,所以能避免闪烁,但也会阻塞绘制、拖慢首帧,并且在 SSR 中需要额外处理。useInsertionEffect 主要供 CSS-in-JS 库使用,不是普通业务 effect 的替代品。

