
一句话:
useLayoutEffect与useEffect的唯一区别是执行时机——前者在「DOM 更新后、浏览器绘制前」同步执行,后者在「绘制后」异步执行;需要「测量 DOM 并立即修正」以防闪烁时用前者。
一、执行时机对比
React 的提交(commit)流程:
① render phase:执行组件函数,算出需要哪些 DOM 变更(可中断)
② commit phase:
a. 把 DOM 变更应用到真实 DOM(mutation)
b. 执行【useLayoutEffect 的清理 + 执行】(同步,阻塞绘制) ← ⭐ 时机一
c. 浏览器绘制(paint)
d. 执行【useEffect 的清理 + 执行】(异步,不阻塞绘制) ← ⭐ 时机二
| 维度 | useLayoutEffect |
useEffect |
|---|---|---|
| 执行时机 | DOM 更新后、绘制前 | 绘制后(异步) |
| 是否阻塞绘制 | ✅ 会阻塞 | ❌ 不阻塞 |
| 能否读到最新布局 | ✅(DOM 已更新) | ✅(也能读到) |
| 用户是否可能看到中间态 | ❌(在绘制前就修正了) | ⚠️ 可能(先绘制错误状态) |
| SSR | ⚠️ 服务端不执行(会警告) | ✅ 服务端不执行但无警告 |
| 优先级 | 少用(只在必要时) | 默认选择 |
// 用日志验证(值得自己跑一遍)
function Demo() {
useLayoutEffect(() => {
console.log('layout effect:绘制前,同步');
return () => console.log('cleanup layout');
});
useEffect(() => {
console.log('effect:绘制后,异步');
return () => console.log('cleanup effect');
});
return 内容;
}
// 输出顺序:
// layout effect:绘制前,同步
// (浏览器绘制)
// effect:绘制后,异步
二、为什么需要 useLayoutEffect(避免闪烁)
// ❌ 用 useEffect:会出现「先看到错误位置,再跳到正确位置」的闪烁
function Tooltip({ children, target }) {
const [pos, setPos] = useState({ top: 0, left: 0 });
const ref = useRef(null);
useEffect(() => {
// ⚠️ 此时浏览器已经绘制过一帧了(位置为初始值 0,0)
const rect = ref.current!.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
setPos({ top: targetRect.bottom, left: targetRect.left - rect.width / 2 });
}, [target]);
return (
{children}
);
}
// 用户可能看到 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 发生在【绘制之后】,
浏览器已经画过一帧(初始/错误状态)→ 用户可能看到闪烁。
三、典型使用场景
// ① 测量 DOM 并据此设置样式(上面 Tooltip 的例子)
useLayoutEffect(() => {
const { height } = ref.current!.getBoundingClientRect();
setHeight(height);
}, []);
// ② 根据内容高度做「展开动画」的初始定位
useLayoutEffect(() => {
const el = panelRef.current!;
el.style.maxHeight = open ? `${el.scrollHeight}px` : '0px';
}, [open]);
// ③ 自动聚焦 / 光标定位(避免「先失焦再聚焦」的视觉跳动)
useLayoutEffect(() => {
inputRef.current?.focus();
inputRef.current?.setSelectionRange(0, 0);
}, []);
// ④ 滚动位置恢复(切换标签页时保持滚动位置)
useLayoutEffect(() => {
if (activeTab === 'search') {
containerRef.current!.scrollTop = savedScrollTop.current;
}
}, [activeTab]);
// ⑤ 与「需要同步读取布局」的第三方库集成
useLayoutEffect(() => {
const instance = new ThirdPartyWidget(el, { size: el.offsetWidth });
return () => instance.destroy();
}, []);
共同特征:都涉及「读取 DOM 的布局信息(尺寸/位置)并立即据此更新 UI」。如果只读取但不需要立即修正(比如打日志、上报),用 useEffect 就行。
四、SSR 中的警告
在服务端渲染(SSR)时,useLayoutEffect 无法执行(服务端没有 DOM),
React 会发出警告:
"useLayoutEffect does nothing on the server"
解法有三种:
① 改用 useEffect(如果不需要「绘制前」的时机)
② 把组件改成「客户端渲染」(Next.js 里用 dynamic import + ssr: false)
③ 用一个「同构 Hook」:在服务端用 useEffect,在客户端用 useLayoutEffect
// ③ 同构 Hook(社区常见做法)
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
// 使用
function Comp() {
const ref = useRef(null);
useIsomorphicLayoutEffect(() => {
// 客户端会同步执行,服务端降级为 useEffect(不执行)
}, []);
return ;
}
// ② Next.js 中让组件只在客户端渲染
import dynamic from 'next/dynamic';
const Tooltip = dynamic(() => import('./Tooltip'), { ssr: false });
五、怎么选:决策表
要读 DOM 的布局信息(尺寸/位置)吗?
├─ 是 → 读取后需要【立即修正 UI】以防闪烁吗?
│ ├─ 是 → useLayoutEffect
│ └─ 否 → useEffect(读取本身不阻塞绘制更好)
└─ 否 → useEffect(绝大多数情况)
进一步的判断:
① 只在「测量 + setState 修正」这个组合下才需要 useLayoutEffect
② 纯订阅、请求、定时器、第三方初始化 → 一律 useEffect
③ 拿不准时用 useEffect(它是更安全、更通用的默认值)
| 场景 | 选择 |
|---|---|
| 数据请求、订阅、定时器 | useEffect |
修改 document.title |
useEffect |
| 与第三方库初始化(不需要立刻测量) | useEffect |
| 测量元素尺寸/位置并立即设置样式 | useLayoutEffect |
| 避免「先渲染错误位置再修正」的闪烁 | useLayoutEffect |
| 滚动位置恢复(切换时需要同步设置) | useLayoutEffect |
| 需要在 SSR 中渲染的组件 | useEffect 或同构 Hook |
性能提醒:
useLayoutEffect阻塞绘制,如果用多了或者里面有重计算,会延长首屏可见时间(用户看到白屏更久)。所以它应该「只在必要时用」,并且里面的逻辑要轻。
六、常见坑
// ① 凡是 Effect 都用 useLayoutEffect
useLayoutEffect(() => {
analytics.track('view'); // ❌ 不需要「绘制前」,白白阻塞绘制
}, []);
// ② 在 useLayoutEffect 里做数据请求
useLayoutEffect(() => {
fetch('/api').then(setData); // ❌ 请求是异步的,布局时机没意义
}, []);
// ③ 用来「消除」useEffect 的闪烁但引入了新问题
// → 如果测量逻辑很重,会让首屏可见时间变长
// ④ SSR 中使用导致警告
useLayoutEffect(() => {}, []); // ⚠️ 服务端会警告
// ✅ 用 useIsomorphicLayoutEffect 或在客户端渲染
// ⑤ 认为 useLayoutEffect 与 useEffect 的「清理时机」也不同
// → 两者的清理时机规则一致(依赖变化后 + 卸载时),只是执行阶段的先后不同
// ⑥ 在 useLayoutEffect 里读 ref 但组件还没挂载
useLayoutEffect(() => {
ref.current.focus(); // ⚠️ 如果组件被条件渲染跳过,ref.current 是 null
}, []);
// ✅ ref.current?.focus();
// ⑦ 大量 DOM 操作放在 useLayoutEffect
// → 会同步阻塞绘制,较长时明显掉帧
// ⑧ 认为「useLayoutEffect 更早执行所以更快」
// → 它更早是「为了正确性」,不是因为更快;反而会阻塞绘制
七、一个完整的对比示例
// 需求:一个「根据内容自动调整宽度」的输入框
function AutoWidthInput() {
const [width, setWidth] = useState(20);
const spanRef = useRef(null);
const [value, setValue] = useState('');
// ❌ 用 useEffect:用户可能看到宽度「跳一下」
// useEffect(() => {
// if (spanRef.current) setWidth(spanRef.current.offsetWidth + 4);
// }, [value]);
// ✅ 用 useLayoutEffect:宽度在绘制前就调整好
useLayoutEffect(() => {
if (spanRef.current) {
setWidth(spanRef.current.offsetWidth + 4); // 测量 + 立即修正
}
}, [value]);
return (
setValue(e.target.value)}
style={{ width }}
/>
{/* 隐藏的测量用元素(宽度随内容变化) */}
{value || ' '}
);
}
面试延伸
- 「
useLayoutEffect和useEffect有什么区别?」
唯一的区别是执行时机:useLayoutEffect在「DOM 已更新、浏览器绘制之前」同步执行(会阻塞绘制);useEffect在「绘制之后」异步执行(不阻塞绘制)。两者的清理函数规则相同(依赖变化后 + 卸载时执行),并且都能读到最新的 DOM。选择标准是「是否需要在用户看到之前修正 UI」。 -
「什么时候必须用
useLayoutEffect?」
当「读取 DOM 布局信息并据此立即更新 UI」时。典型场景:① 测量元素尺寸/位置后设置样式(Tooltip 定位、自适应宽度);② 避免「先渲染出来再跳到正确位置」的闪烁;③ 切换标签页时恢复滚动位置(需要同步设置);④ 自动聚焦并设置光标位置。判断标准是「如果晚一帧修正,用户会看到闪烁」。 -
「为什么用
useEffect会导致闪烁?」
因为useEffect在浏览器绘制之后执行:此时用户已经看到了一帧「初始状态」(比如位置为 0,0 的 Tooltip),然后你在 Effect 里setState触发重渲染,用户又看到「正确位置」——两帧之间产生了肉眼可见的跳动。而useLayoutEffect在绘制前完成测量与更新,浏览器只绘制最终结果。 -
「
useLayoutEffect有什么代价?」
它阻塞浏览器绘制:effect 执行、其中的 DOM 操作、以及由此触发的同步重渲染都必须在「用户看到下一帧之前」完成。如果逻辑较重(大量 DOM 读写、复杂计算),用户的首屏可见时间会被延长(表现为白屏更久)。所以它应该「只在必要时使用」,并且内部逻辑要尽量轻。 -
「
useLayoutEffect在 SSR 中会怎样?」
服务端没有 DOM,所以useLayoutEffect在服务端不会执行,React 会发出警告(useLayoutEffect does nothing on the server)。三种解法:① 改用useEffect(如果不需要「绘制前」的时机);② 让组件只在客户端渲染(Next.js 用dynamic(..., { ssr: false }));③ 用同构 Hook——typeof window !== 'undefined' ? useLayoutEffect : useEffect。 -
「React 的内部执行顺序是怎样的?」
一次更新大致是:①render阶段(可中断,执行组件函数算出 DOM 变更)→ ②commit阶段的三步:mutation(应用 DOM 变更)→ layout(执行useLayoutEffect,同步)→ 浏览器绘制 → passive(执行useEffect,异步)。这个划分也解释了「为什么useLayoutEffect能避免闪烁」——它处在「DOM 已更新但还没绘制」的窗口里。 -
「
useLayoutEffect能替代useEffect吗?」
技术上可以(它更早执行,也能读到最新 DOM),但不应该:① 它会阻塞绘制,滥用会拖慢首屏;② SSR 中会警告;③ 大多数逻辑(请求、订阅、定时器)根本不需要「绘制前」的时机。反过来useEffect不能替代useLayoutEffect(时机不够早)。所以默认用useEffect,只在需要「绘制前修正」时用useLayoutEffect。 -
「怎么在需要时才用
useLayoutEffect,同时避免 SSR 警告?」
用同构 Hook 封装:const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;,在组件里统一用这个 Hook。这样客户端会同步执行(避免闪烁),服务端降级为useEffect(不会警告也不会执行)。这是 Next.js / Remix 项目里的标准做法(很多 UI 库都这么做)。
一句话速记
useLayoutEffect与useEffect唯一区别是时机:前者在「DOM 更新后、绘制前」同步执行(阻塞绘制),后者在「绘制后」异步执行;只在「测量 DOM 并立即修正以防闪烁」时用前者(Tooltip 定位、自适应宽度、滚动恢复、自动聚焦);它的代价是阻塞绘制(用多了拖慢首屏);SSR 中会警告,解法是同构 Hook(typeof window !== 'undefined' ? useLayoutEffect : useEffect)或只在客户端渲染。



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