陌上人如玉
公子世无双

2.4 useLayoutEffect

本文于 2026-10-12 11:26 更新,部分内容具有时效性,如有失效,请留言

2.4 useLayoutEffect

一句话: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 || ' '}
); }

面试延伸

  1. 「useLayoutEffect 和 useEffect 有什么区别?」
    唯一的区别是执行时机:useLayoutEffect 在「DOM 已更新、浏览器绘制之前」同步执行(会阻塞绘制);useEffect 在「绘制之后」异步执行(不阻塞绘制)。两者的清理函数规则相同(依赖变化后 + 卸载时执行),并且都能读到最新的 DOM。选择标准是「是否需要在用户看到之前修正 UI」。

  2. 「什么时候必须用 useLayoutEffect?」
    当「读取 DOM 布局信息并据此立即更新 UI」时。典型场景:① 测量元素尺寸/位置后设置样式(Tooltip 定位、自适应宽度);② 避免「先渲染出来再跳到正确位置」的闪烁;③ 切换标签页时恢复滚动位置(需要同步设置);④ 自动聚焦并设置光标位置。判断标准是「如果晚一帧修正,用户会看到闪烁」。

  3. 「为什么用 useEffect 会导致闪烁?」
    因为 useEffect 在浏览器绘制之后执行:此时用户已经看到了一帧「初始状态」(比如位置为 0,0 的 Tooltip),然后你在 Effect 里 setState 触发重渲染,用户又看到「正确位置」——两帧之间产生了肉眼可见的跳动。而 useLayoutEffect 在绘制前完成测量与更新,浏览器只绘制最终结果。

  4. 「useLayoutEffect 有什么代价?」
    它阻塞浏览器绘制:effect 执行、其中的 DOM 操作、以及由此触发的同步重渲染都必须在「用户看到下一帧之前」完成。如果逻辑较重(大量 DOM 读写、复杂计算),用户的首屏可见时间会被延长(表现为白屏更久)。所以它应该「只在必要时使用」,并且内部逻辑要尽量轻。

  5. 「useLayoutEffect 在 SSR 中会怎样?」
    服务端没有 DOM,所以 useLayoutEffect 在服务端不会执行,React 会发出警告(useLayoutEffect does nothing on the server)。三种解法:① 改用 useEffect(如果不需要「绘制前」的时机);② 让组件只在客户端渲染(Next.js 用 dynamic(..., { ssr: false }));③ 用同构 Hook——typeof window !== 'undefined' ? useLayoutEffect : useEffect。

  6. 「React 的内部执行顺序是怎样的?」
    一次更新大致是:① render 阶段(可中断,执行组件函数算出 DOM 变更)→ ② commit 阶段的三步:mutation(应用 DOM 变更)→ layout(执行 useLayoutEffect,同步)→ 浏览器绘制 → passive(执行 useEffect,异步)。这个划分也解释了「为什么 useLayoutEffect 能避免闪烁」——它处在「DOM 已更新但还没绘制」的窗口里。

  7. 「useLayoutEffect 能替代 useEffect 吗?」
    技术上可以(它更早执行,也能读到最新 DOM),但不应该:① 它会阻塞绘制,滥用会拖慢首屏;② SSR 中会警告;③ 大多数逻辑(请求、订阅、定时器)根本不需要「绘制前」的时机。反过来 useEffect 不能替代 useLayoutEffect(时机不够早)。所以默认用 useEffect,只在需要「绘制前修正」时用 useLayoutEffect。

  8. 「怎么在需要时才用 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)或只在客户端渲染。

赞(0) 打赏
未经允许不得转载:陌上寒 » 2.4 useLayoutEffect

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫