useEffect vs useLayoutEffect:时机、阻塞与最佳实践全指南

16 阅读5分钟
拒绝页面闪烁!一文彻底搞懂 useLayoutEffect 与 useEffect 的核心差异

在 React 开发中,useEffect 和 useLayoutEffect 的 API 签名完全一致,这让许多开发者感到困惑:它们到底有什么区别?什么时候该用哪一个?

答案并不在于“怎么写”,而在于 “何时执行” 以及 “是否阻塞浏览器绘制” 。选错 Hook,轻则导致页面闪烁,重则引发性能卡顿甚至 SSR 报错。

本文将带你深入 React 的渲染流水线,彻底厘清两者的核心差异。


1. 核心区别:执行时机与渲染流水线

要理解这两个 Hook,必须先看清 React 的完整渲染流程:

  1. Render (渲染) :React 计算 JSX,生成虚拟 DOM。
  2. Commit (提交) :React 将变更同步应用到真实 DOM。
  3. Browser Paint (浏览器绘制) :浏览器根据最新 DOM 计算布局(Layout)并绘制到屏幕(用户此时看到变化)。
  4. Effects 执行:运行副作用代码。

两者的分水岭就在于 第 3 步(浏览器绘制)  的前后。

🟢 useEffect:异步执行,不阻塞绘制

  • 执行时机:在 DOM 更新后 且 浏览器完成绘制后,异步触发。
  • 用户体验:用户会先看到 DOM 更新后的界面(可能是中间状态),随后 Effect 才运行。
  • 关键特性非阻塞。即使 Effect 中包含耗时操作,也不会阻挡浏览器的绘制线程,界面保持流畅。
  • 适用场景:95% 的副作用场景(数据请求、订阅事件、日志上报、非关键的 DOM 操作)。

🔵 useLayoutEffect:同步执行,阻塞绘制

  • 执行时机:在 DOM 更新后 但 浏览器绘制之前,同步触发。
  • 用户体验:浏览器会暂停绘制,等待 useLayoutEffect 中的所有代码(包括 DOM 测量和修改)执行完毕,才会进行下一次绘制。用户直接看到的是修正后的最终界面。
  • 关键特性阻塞。如果代码执行过慢,会导致页面出现明显的“卡顿”或白屏,因为浏览器无法及时响应用户的视觉更新。
  • 适用场景:必须同步完成的 DOM 操作(如测量元素宽高、避免视觉闪烁的布局调整)。

2. 直观流程对比

通过流程图,我们可以更清晰地看到差异:

【useEffect 流程】
React Render 
  ↓
DOM 更新 (Commit)
  ↓
[🖌️ 浏览器绘制] 👈 用户此时看到界面(可能包含未修正的样式)
  ↓
useEffect 执行 (异步) 
  ↓
(若修改了 DOM/State,可能触发二次渲染)

---------------------------------------

【useLayoutEffect 流程】
React Render 
  ↓
DOM 更新 (Commit)
  ↓
useLayoutEffect 执行 (同步) 👈 此时进行 DOM 测量与修正
  ↓
[🖌️ 浏览器绘制] 👈 用户直接看到修正后的最终界面

3. 什么时候必须使用 useLayoutEffect

请记住一个原则:除非必要,否则不要用 useLayoutEffect  只有在以下两种场景中,它才是不可或缺的:

场景一:消除视觉闪烁 (Visual Glitch)

当你需要根据 DOM 的实际内容动态调整样式时(例如:根据文本长度定位气泡、根据内容高度自适应容器、模态框居中),如果使用 useEffect

  1. 浏览器先绘制出默认位置的元素。
  2. useEffect 执行,计算出正确位置并更新 State。
  3. React 再次渲染,元素跳到正确位置。

结果:用户会看到元素“闪”了一下。

使用 useLayoutEffect 可以确保在浏览器绘制前,位置已经修正完毕,用户看到的始终是最终状态。

✅ 代码示例:动态测量宽度

import { useRef, useLayoutEffect, useState } from 'react';

function Tooltip({ text }) {
  const ref = useRef(null);
  const [width, setWidth] = useState(0);

  // ✅ 必须使用 useLayoutEffect
  // 若改用 useEffect,用户会先看到 width=0 的初始状态,随后瞬间跳变
  useLayoutEffect(() => {
    if (ref.current) {
      // 1. 同步测量 DOM
      const measuredWidth = ref.current.offsetWidth;
      // 2. 同步更新 state,触发重新渲染但在绘制前完成
      setWidth(measuredWidth);
    }
  }, [text]);

  return (
    <div ref={ref} style={{ width: width ? width : 'auto' }}>
      {text}
    </div>
  );
}

场景二:第三方库的同步初始化

某些对布局敏感的第三方库(如 D3.js、Mapbox、复杂的动画引擎),需要在 DOM 挂载后立即获取准确的布局信息并进行初始化。如果等到浏览器绘制后再执行,可能会导致初始渲染错位或动画不同步。


4. 什么时候使用 useEffect

这是你的默认选项。  以下场景请无脑选择 useEffect

  • 💾 数据获取fetchaxios 请求。
  • 🔔 订阅与监听window.addEventListener, WebSocket 连接。
  • 📝 副作用操作:修改 document.title,发送分析日志。
  • 🎨 非关键 DOM 操作:任何不需要在首屏绘制前立即完成的交互。

💡 黄金法则:优先使用 useEffect。只有当你肉眼观察到页面闪烁,或者业务逻辑强依赖“绘制前的 DOM 测量”时,才降级切换到 useLayoutEffect


5. ⚠️ 高危陷阱:服务端渲染 (SSR)

在服务端渲染(Next.js, Remix 等)环境中,这是一个极易被忽视的坑:

  • useEffect:安全。它在服务端完全不执行,只在客户端 hydration 后运行。

  • useLayoutEffect:危险。它在服务端也会尝试执行。由于服务端没有 window 或 DOM 对象,通常会直接报错或抛出警告:

    Warning: useLayoutEffect does nothing on the server...

🛡️ 解决方案:编写兼容 Hook

在 SSR 项目中,建议封装一个通用的 Hook,自动判断环境:

import { useEffect, useLayoutEffect } from 'react';

// 如果是浏览器环境,使用 useLayoutEffect;否则降级为 useEffect
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function MyComponent() {
  useIsomorphicLayoutEffect(() => {
    // 安全地在客户端执行布局逻辑,服务端静默跳过
    console.log('DOM operations here');
  }, []);
  
  return <div>SSR Safe Component</div>;
}

6. 总结对比表

特性useEffectuseLayoutEffect
执行时机浏览器绘制 (异步)浏览器绘制 (同步)
阻塞渲染❌ 否 (非阻塞,性能好)✅ 是 (阻塞,需谨慎)
视觉表现可能会有短暂闪烁无闪烁 (用户见即最终态)
主要用途数据请求、订阅、日志DOM 测量、布局修正、防闪烁
SSR 兼容性✅ 安全 (服务端不执行)⚠️ 会报警告 (需降级处理)
推荐优先级⭐⭐⭐⭐⭐ (首选)⭐⭐ (仅在必要时使用)

🚀 最佳实践建议

  1. 默认即 useEffect:不要为了“显得更同步”而滥用 useLayoutEffect
  2. 观察驱动切换:只有当测试中发现明显的 UI 跳动或布局计算必须在绘制前完成时,才切换 Hook。
  3. SSR 意识:在编写通用组件库或 SSR 应用时,务必处理 useLayoutEffect 的服务端兼容性问题。

理解并正确使用这两个 Hook,是写出高性能、无闪烁 React 应用的关键一步。