useLayoutEffect 和 useEffect 有什么区别?
简化版
useEffect 在浏览器绘制后异步执行,适合请求、订阅、日志等不阻塞视觉的副作用;useLayoutEffect 在 DOM 更新后、浏览器绘制前同步执行,适合读取布局并立即修正样式。滥用 useLayoutEffect 会阻塞绘制。
详细版
两者都在提交阶段后执行,但时机不同。
useEffect(() => {
console.log('绘制后执行')
}, [])
useLayoutEffect(() => {
const rect = ref.current!.getBoundingClientRect()
setHeight(rect.height)
}, [])
如果副作用不影响首帧布局,用 useEffect。如果需要在用户看到页面前完成测量和同步修正,例如 Tooltip 定位、虚拟列表初始测量、避免闪烁的布局调整,才考虑 useLayoutEffect。
完整版教学
一、区别的核心是是否阻塞浏览器绘制
React 更新 DOM 后,浏览器还要计算样式、布局和绘制。
useLayoutEffect 会在绘制前同步执行,执行期间浏览器不能把新画面显示给用户。
useEffect 会推迟到绘制之后,不挡住首帧。
React render -> commit DOM -> useLayoutEffect -> paint -> useEffect
如果一个 effect 执行 20ms,放在 useLayoutEffect 里可能直接让首帧晚 20ms。
放在 useEffect 里,用户至少能先看到更新后的页面。
所以它不是“高级版 useEffect”,而是更强也更危险的同步钩子。
二、useEffect 适合绝大多数副作用
网络请求、事件订阅、日志上报、手动同步非关键状态,大多不需要阻塞绘制。
用户看到页面后再执行这些工作,体验通常更好。
这也是日常开发优先使用 useEffect 的原因。
useEffect(() => {
const onResize = () => console.log(window.innerWidth)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [])
| 场景 | 推荐 |
|---|---|
| 请求接口 | useEffect |
| 订阅事件 | useEffect |
| 日志埋点 | useEffect |
| 首帧前修正位置 | useLayoutEffect |
如果某个副作用晚一点执行不会让用户看到错误画面,就不要用同步布局 effect。 这是性能和体验之间最稳的默认选择。
三、useLayoutEffect 适合读布局再同步写布局
典型例子是 Tooltip。
你需要先渲染一个浮层,读它的宽高和目标元素位置,然后在浏览器绘制前把它放到正确位置。
如果用 useEffect,用户可能先看到浮层出现在错误位置,下一帧才跳过去。
useLayoutEffect(() => {
const target = targetRef.current!.getBoundingClientRect()
const tip = tipRef.current!.getBoundingClientRect()
setStyle({
left: target.left + target.width / 2 - tip.width / 2,
top: target.bottom + 8
})
}, [open])
错误体验:
先画在 0,0 -> effect 后移动 -> 用户看到闪一下
layout effect:
测量并修正 -> 再绘制 -> 用户只看到正确位置
这类场景的关键词是“避免视觉闪烁”。
如果没有这个问题,useLayoutEffect 多半不是必要的。
四、同步执行意味着性能压力更直接
useLayoutEffect 里读取布局再写样式,可能触发浏览器同步布局计算。
如果在列表里有 100 个子组件都做测量,成本会很明显。
尤其在低端设备上,几十毫秒就足以造成卡顿。
// 谨慎:每一项都同步测量
items.map(item => <MeasuredRow key={item.id} item={item} />)
假设每个测量平均 0.3ms,100 项就是 30ms。 这已经超过一帧 16.7ms 的预算。 工程里应尽量批量测量、缓存结果,或把测量限制在可视区域。
五、SSR 场景要额外小心
在服务端没有真实 DOM,也没有布局。
useLayoutEffect 在服务端没有意义,框架通常会给出警告。
如果组件既要 SSR 又要布局测量,可以只在客户端挂载后渲染相关内容,或封装同构安全的 hook。
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
这段写法不是银弹,但表达了边界:服务端无法读取布局,客户端才做同步测量。 如果项目使用 Next.js,这个点很容易被追问。
六、选择时按“用户会不会看到错误中间态”判断
很多副作用都可以晚一点。
真正需要 useLayoutEffect 的,是晚一点就会让用户看到错误画面的操作。
例如测量高度后立即展开、定位浮层、滚动位置恢复、首帧前修正布局。
记忆钩子:useEffect 不挡画面,useLayoutEffect 挡住画面先修布局。
这个判断比死背生命周期更有用。 面试时先说时机,再说场景,最后补性能和 SSR,答案就很完整。
七、常见误区与追问
- 误区:useLayoutEffect 是 useEffect 的高级替代。 它会阻塞绘制,只应在必须同步测量或修正布局时使用。
- 误区:请求接口应该放 useLayoutEffect。 请求不会修正首帧布局,放
useEffect更合适。 - 误区:useLayoutEffect 一定能提升体验。 如果逻辑耗时,会推迟绘制,反而让页面更卡。
- 追问:为什么 Tooltip 常用 useLayoutEffect? 它需要先测量目标和浮层尺寸,再在绘制前修正位置,避免闪烁。
- 追问:SSR 中为什么会有警告? 服务端没有 DOM 和布局,
useLayoutEffect的语义无法执行。 - 追问:两者清理函数有区别吗? 都可以返回清理函数,主要差别在执行时机。
八、加强记忆
这题按“绘制前后”记。useLayoutEffect 在 DOM 提交后、paint 前同步执行,适合测量布局并防闪;useEffect 在 paint 后执行,适合大多数副作用。能不用同步就不用同步,性能会感谢你。