
一句话:两者唯一的区别是执行时机——
useLayoutEffect在「DOM 更新后、浏览器绘制前」同步执行(会阻塞绘制),useEffect在「绘制后」异步执行;需要「测量 DOM 并立即修正以防闪烁」时用前者。
一、结论先行
| 维度 | useLayoutEffect |
useEffect |
|---|---|---|
| 执行时机 | DOM 更新后、绘制前 | 绘制后(异步) |
| 是否阻塞绘制 | ✅ 阻塞 | ❌ 不阻塞 |
| 执行方式 | 同步 | 异步(调度) |
| 用户是否可能看到中间态 | ❌ 不会(绘制前已修正) | ⚠️ 可能(闪一下) |
| 默认选择 | ❌ | ✅ |
| SSR | ⚠️ 服务端不执行(会警告) | ✅ 不执行但无警告 |
| 典型用途 | 测量布局、避免闪烁 | 请求、订阅、日志 |
⭐ 一句话回答:
「它们的区别只在时机——useLayoutEffect 在『DOM 已更新但还没绘制』时同步执行,
useEffect 在『绘制之后』异步执行;所以需要『读取布局并立即修正』时用前者,
其它情况一律用后者。」
二、执行时机详解
一次更新的完整时序:
① render 阶段(执行组件函数,算出需要哪些 DOM 变更)—— 可中断
② commit 阶段:
a. before mutation
b. mutation(把 DOM 变更应用到真实 DOM)
· 执行 useLayoutEffect 的【清理函数】
c. layout(DOM 已更新、绘制前)
· 赋值 ref
· ⭐ 执行 useLayoutEffect 的【effect 函数】(同步,阻塞绘制)
③ 【浏览器绘制 paint】← 用户在这一刻看到界面
④ passive effects(异步)
· 执行 useEffect 的【清理函数】
· ⭐ 执行 useEffect 的【effect 函数】
// ✅ 用日志验证(强烈建议自己跑一遍)
function Demo({ id }: { id: number }) {
console.log('① render', id);
useLayoutEffect(() => {
console.log('② layoutEffect', id);
return () => console.log('② cleanup layout', id);
}, [id]);
useEffect(() => {
console.log('③ effect', id);
return () => console.log('③ cleanup effect', id);
}, [id]);
return <div>{id}</div>;
}
// 首次挂载:① render 1 → ② layoutEffect 1 → ③ effect 1
// id 从 1 变 2:
// ① render 2
// ② cleanup layout 1 → ② layoutEffect 2
// ③ cleanup effect 1 → ③ effect 2
三、为什么 useLayoutEffect 能避免闪烁
// ❌ 用 useEffect:用户可能看到「先渲染错误位置,再跳到正确位置」
function Tooltip({ target, children }: { target: HTMLElement; children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [pos, setPos] = useState({ top: 0, left: 0 });
useEffect(() => {
// ⚠️ 此时浏览器【已经绘制过一帧】(pos 还是初始值 0,0)
const rect = ref.current!.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
setPos({ top: targetRect.bottom, left: targetRect.left - rect.width / 2 });
}, [target]);
return <div ref={ref} style={{ position: 'fixed', ...pos }}>{children}</div>;
}
// 现象:tooltip 先在左上角闪一下,然后跳到目标位置
// ✅ 用 useLayoutEffect:在绘制前完成测量与更新
useLayoutEffect(() => {
const rect = ref.current!.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
setPos({ top: targetRect.bottom, left: targetRect.left - rect.width / 2 });
}, [target]);
// → 浏览器只绘制最终结果(用户看不到中间态)
⭐ 原理:
· useLayoutEffect 里的 setState 会「同步触发一次重渲染」,
且这次重渲染【在浏览器绘制之前】完成 → 用户看不到「初值那一帧」
· useEffect 里的 setState 发生在「绘制之后」→
用户已经看到了「初值那一帧」→ 会有「跳动/闪烁」
四、什么时候必须用 useLayoutEffect
// ① 测量元素尺寸/位置并据此设置样式(上面 Tooltip 的例子)
useLayoutEffect(() => {
const { height } = ref.current!.getBoundingClientRect();
setHeight(height);
}, [content]);
// ② 根据内容高度做「展开动画」的初始定位
useLayoutEffect(() => {
const el = panelRef.current!;
el.style.maxHeight = open ? `${el.scrollHeight}px` : '0px';
}, [open]);
// ③ 自动聚焦 + 设置光标位置(避免「先失焦再聚焦」的视觉跳动)
useLayoutEffect(() => {
inputRef.current?.focus();
inputRef.current?.setSelectionRange(value.length, value.length);
}, []);
// ④ 切换标签/路由时「同步恢复滚动位置」
useLayoutEffect(() => {
containerRef.current!.scrollTop = savedScrollTop.current;
}, [activeTab]);
⭐ 共同特征:「**读取 DOM 布局信息(尺寸/位置)并立即据此更新 UI**」。
判断标准:「**如果晚一帧修正,用户会看到闪烁吗?**」
· 会 → useLayoutEffect
· 不会 → useEffect(更安全、不阻塞绘制)
五、useLayoutEffect 的代价
⚠️ 它【阻塞浏览器绘制】:
① effect 的执行时间
② 其中触发的「同步重渲染」
都要在「用户看到下一帧之前」完成。
后果:
· 逻辑重(大量 DOM 读写、复杂计算)→ 首屏可见时间变长(白屏更久)
· 在「大列表 / 复杂组件」里滥用 → 明显拖慢
所以规则是:
① 只在「必须绘制前完成」时用
② 里面的逻辑要「轻」(测量 + 一次 setState,不要做重计算)
// ❌ 在 useLayoutEffect 里做重计算(阻塞绘制)
useLayoutEffect(() => {
const result = heavyCompute(data); // ⚠️ 阻塞首屏
setResult(result);
}, [data]);
// ✅ 放到 useEffect(不阻塞绘制),或用 useMemo(在渲染期算但不阻塞绘制)
const result = useMemo(() => heavyCompute(data), [data]);
// ❌ 在 useLayoutEffect 里做「数据请求」(时机没意义,反而阻塞)
useLayoutEffect(() => {
fetchData().then(setData); // ⚠️ 请求是异步的,布局时机没意义
}, []);
// ✅ 请求放 useEffect
useEffect(() => {
const ac = new AbortController();
fetchData({ signal: ac.signal }).then(setData);
return () => ac.abort();
}, []);
六、SSR 中的警告与同构 Hook
⚠️ 服务端没有 DOM,useLayoutEffect 不会执行,React 会发出警告:
"useLayoutEffect does nothing on the server"
三种解法:
① 改用 useEffect(如果不需要「绘制前」的时机)
② 让组件只在客户端渲染(Next.js:dynamic(..., { ssr: false }))
③ 用「同构 Hook」:服务端用 useEffect、客户端用 useLayoutEffect
// ✅ 同构 Hook(社区标准做法,很多 UI 库都这么做)
import { useEffect, useLayoutEffect } from 'react';
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// 使用
function Tooltip() {
const ref = useRef<HTMLDivElement>(null);
useIsomorphicLayoutEffect(() => {
// 客户端:同步执行(避免闪烁)
// 服务端:降级为 useEffect(不执行、不警告)
}, []);
return <div ref={ref} />;
}
七、常见追问
Q: 它们的「清理函数」时机也不同吗?
A: 规则相同(依赖变化后 + 卸载时),只是「执行的阶段不同」:
layout 的清理在「mutation 阶段」执行,
passive 的清理在「绘制后」执行。
Q: 都能读到最新 DOM 吗?
A: 都能。区别是「useLayoutEffect 在绘制前读取」(可以用来修正 UI 而不闪),
「useEffect 在绘制后读取」(读得到,但改样式会闪)。
Q: 都能读 ref 吗?
A: 都能(ref 在 commit 的 mutation/layout 阶段赋值)。
但 useLayoutEffect 里 ref 一定已就绪(因为它就在 layout 阶段)。
Q: 在 useLayoutEffect 里 setState 会「多一次渲染」吗?
A: 会(同步重渲染),但它发生在「绘制前」 →
用户只看到最终结果;代价是「这一帧的计算量变大」。
Q: 用 useEffect 也看不到闪烁,是不是就不用 useLayoutEffect 了?
A: 只要「effect 里 setState 影响了可见样式」就可能闪。
在现代浏览器里「有时看起来不闪」(因为绘制可能被推迟),
但这【不可靠】——应该在「需要保证不闪」时用 useLayoutEffect。
Q: 三个 Effect 的顺序是怎样的(还有一个 useInsertionEffect)?
A: useInsertionEffect(给 CSS-in-JS 用,在 mutation 前/布局前)
→ useLayoutEffect(绘制前)→ useEffect(绘制后)。
Q: 为什么 React 18 的 StrictMode 会让它们都执行两次?
A: 它故意模拟「挂载 → 卸载 → 再挂载」以暴露「清理缺失」。
Q: 在 SSR 里用 useLayoutEffect 会「报错」吗?
A: 只是【警告】(且服务端不执行它);用同构 Hook 可消除警告。
八、常见坑
// ① 用 useEffect 做「测量后立即设置样式」(会闪)
useEffect(() => { setHeight(ref.current!.offsetHeight); }, []); // ⚠️ 可能闪
// ② 用 useLayoutEffect 做「请求/重计算」(阻塞绘制)
useLayoutEffect(() => { fetch(...).then(setData); }, []); // ❌
// ③ 在 SSR 里直接用 useLayoutEffect(会警告)
// ✅ 用同构 Hook 或客户端渲染
// ④ 认为「useLayoutEffect 更早执行所以更好」(它会阻塞绘制)
// ⑤ 认为「两者清理时机不同」(规则相同,只是阶段不同)
// ⑥ 在 useLayoutEffect 里「读 ref.current 时忘了判空」
useLayoutEffect(() => { ref.current.focus(); }, []); // ⚠️
// ⑦ 用 useLayoutEffect 做「需要在绘制后才做」的事(如统计上报)
// ⑧ 在「已经挂起(Suspense)的组件」里用布局 effect(时机语义会变)
// ⑨ 忘了「依赖数组」(默认每次渲染都执行 → 阻塞绘制)
useLayoutEffect(() => { /* 没有依赖 */ }); // ⚠️
// ⑩ 认为「useLayoutEffect 能替代 useEffect」——
// 技术上可以(更早),但会「无谓阻塞绘制」且 SSR 会警告
// ⑪ 在「布局 effect 里改样式」导致「强制同步布局」(layout thrashing)
useLayoutEffect(() => {
const h = el.offsetHeight; // 读 → 触发 layout
el.style.height = `${h}px`; // 写 → 又要 layout
const h2 = el.offsetHeight; // 再读 → 又触发
}, []);
// ⑫ 依赖「useLayoutEffect 一定在绘制前」但在「并发渲染」下有更细的语义
九、面试延伸
- 「
useEffect和useLayoutEffect有什么区别?」
唯一的区别是执行时机:useLayoutEffect在「DOM 已更新、浏览器还没绘制」时同步执行(会阻塞绘制);useEffect在「绘制之后」异步执行(不阻塞)。两者的清理函数规则相同(依赖变化后 + 卸载时执行),并且都能读到最新的 DOM。所以选择的唯一标准是「是否需要在用户看到之前修正 UI」。 -
「什么时候必须用
useLayoutEffect?」
当「读取 DOM 布局信息并据此立即更新 UI」时,具体有四个典型场景:① 测量元素尺寸/位置后设置样式(Tooltip 定位、自适应宽度);② 根据内容高度做展开动画的初始定位(maxHeight);③ 自动聚焦并设置光标位置;④ 切换标签/路由时同步恢复滚动位置。判断标准是「如果晚一帧修正,用户会看到闪烁吗」——会就用它,不会就用useEffect。 -
「为什么
useEffect会导致闪烁?」
因为它在「浏览器绘制之后」执行:此时用户已经看到了一帧「初始状态」(如pos还是 0,0 的 Tooltip),然后你在 effect 里setState触发重渲染,用户又看到「正确位置」——两帧之间产生了肉眼可见的跳动。而useLayoutEffect在「绘制前」完成「测量 + setState + 同步重渲染」,浏览器只绘制最终结果,因此看不到中间态。 -
「
useLayoutEffect有什么代价?」
它阻塞浏览器绘制——effect 的执行、其中的 DOM 操作、以及由此触发的「同步重渲染」都必须在「用户看到下一帧之前」完成。所以如果逻辑重(大量 DOM 读写、复杂计算),用户的首屏可见时间会被延长(表现为白屏更久)。因此它应该「只在必要时使用」,并且内部的逻辑要「轻」(测量 + 一次setState,不要做重计算或请求)。 -
「SSR 中用
useLayoutEffect会怎样?」
服务端没有 DOM,所以它不会执行,React 会发出警告(useLayoutEffect does nothing on the server)。三种解法:① 改用useEffect(如果不需要「绘制前」的时机);② 让组件只在客户端渲染(Next.js 用dynamic(..., { ssr: false }));③ 用同构 Hook——const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;。社区库里普遍用第 ③ 种(既避免闪烁又不产生警告)。 -
「React 的内部执行顺序是怎样的?」
一次更新:①render阶段(可中断,执行组件函数算出 DOM 变更)→ ②commit阶段:mutation(应用 DOM 变更,并执行 layout effect 的清理)→ layout(赋 ref、同步执行useLayoutEffect)→ ③ 浏览器绘制 → ④ passive effects(异步执行useEffect的清理与 effect)。这个划分解释了「为什么useLayoutEffect能避免闪烁」——它处在「DOM 已更新但还没绘制」的窗口里。 -
「
useInsertionEffect是什么?」
它是 React 18 引入的第三个 effect,专为 CSS-in-JS 库设计——它在「DOM 变更前、布局 effect 之前」同步执行,用于「插入<style>标签」(这样样式在布局计算前就位,避免「样式晚到导致的一次额外布局」)。业务代码几乎不需要它,但要知道「存在三种 effect,时机从早到晚是:useInsertionEffect→useLayoutEffect→useEffect」。 -
「如果
useEffect里 setState 也不闪,还需要useLayoutEffect吗?」
需要(在「必须保证不闪」的场景)。因为「useEffect里 setState 有时看起来不闪」是巧合——它取决于「浏览器是否在 effect 之前绘制了那一帧」(受「JS 执行是否超出一帧」等因素影响,不可靠)。所以在「测量后立即改样式」这类场景应当明确用useLayoutEffect(或让「初值就是合理值」,从而根本上不需要那次修正)。工程上更稳的做法是「避免需要二次修正」(如用 CSS 直接算出位置、或用ResizeObserver提前拿到尺寸)。
一句话速记
唯一的区别是时机:
useLayoutEffect在「DOM 更新后、绘制前」同步执行(阻塞绘制),useEffect在「绘制后」异步执行(清理规则相同、都能读最新 DOM);需要「测量 DOM 并立即修正以防闪烁」时用前者(Tooltip 定位、自适应宽度、展开动画初始高度、自动聚焦、滚动恢复),其它一律用后者;代价是阻塞绘制(逻辑要轻,别放重计算或请求);SSR 会警告(服务端不执行)→ 用同构 Hook(typeof window !== 'undefined' ? useLayoutEffect : useEffect);三种 effect 的时机从早到晚是「useInsertionEffect(CSS-in-JS 插样式)→useLayoutEffect→useEffect」;「useEffect里 setState 有时不闪」是巧合、不可靠。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!