为什么你的页面会闪烁?useLayoutEffect和useEffect的区别藏在这里!

你是不是遇到过这种情况:页面刚渲染出来时,某些元素的位置或尺寸会突然"跳一下",出现短暂闪烁?或者明明代码逻辑没问题,但UI更新总感觉慢半拍?

这很可能是因为你没选对React的副作用钩子!今天我们就来彻底搞懂useLayoutEffect和useEffect的区别,让你从此告别页面闪烁问题。

浏览器绘制时机的秘密

要理解这两个钩子的区别,得先知道浏览器是怎么工作的。当React更新DOM时,浏览器其实做了两件事:

首先是布局计算 :浏览器计算每个元素的位置和大小 然后是实际绘制:把计算好的像素点画到屏幕上

useEffect是在浏览器完成绘制之后 才执行的,相当于"事后诸葛亮"。而useLayoutEffect则是在DOM更新后、浏览器绘制之前执行的,相当于"现场指挥"。

这么说可能有点抽象,我们来看个具体例子。

代码实战:测量DOM尺寸并立即更新

假设我们需要实时获取一个div的宽度,并根据宽度调整样式。先用useEffect试试:

javascript 复制代码
function MyComponent() {
  const [width, setWidth] = useState(0);
  const divRef = useRef(null);

  useEffect(() => {
    // 在useEffect中测量元素尺寸
    if (divRef.current) {
      const measuredWidth = divRef.current.offsetWidth;
      setWidth(measuredWidth);
      console.log('useEffect中测量的宽度:', measuredWidth);
    }
  }, []);

  return (
    <div 
      ref={divRef}
      style={{ 
        width: '100%', 
        height: 100, 
        backgroundColor: width > 500 ? 'blue' : 'red' 
      }}
    >
      当前宽度: {width}px
    </div>
  );
}

运行这段代码,你会发现页面上的div颜色可能会从红色瞬间变成蓝色(或者反过来),出现一次闪烁。这是因为:

  1. 组件首次渲染时,width是0,div显示红色
  2. 浏览器绘制了红色div
  3. useEffect执行,测量到实际宽度并更新state
  4. 组件重新渲染,div颜色根据新宽度改变
  5. 浏览器再次绘制,颜色变化

用户就能看到这次颜色变化,造成闪烁。

现在改用useLayoutEffect:

javascript 复制代码
function MyComponent() {
  const [width, setWidth] = useState(0);
  const divRef = useRef(null);

  useLayoutEffect(() => {
    // 在useLayoutEffect中测量元素尺寸
    if (divRef.current) {
      const measuredWidth = divRef.current.offsetWidth;
      setWidth(measuredWidth);
      console.log('useLayoutEffect中测量的宽度:', measuredWidth);
    }
  }, []);

  return (
    <div 
      ref={divRef}
      style={{ 
        width: '100%', 
        height: 100, 
        backgroundColor: width > 500 ? 'blue' : 'red' 
      }}
    >
      当前宽度: {width}px
    </div>
  );
}

这次就不会有闪烁了!因为:

  1. 组件首次渲染时,width是0,div显示红色
  2. 在浏览器绘制之前,useLayoutEffect就执行了
  3. 它测量宽度并立即更新state
  4. 组件基于正确的宽度重新渲染
  5. 浏览器一次性绘制最终效果

用户直接看到最终状态,没有中间闪烁过程。

千万不要在useLayoutEffect里做这些事!

useLayoutEffect虽然能解决闪烁问题,但有个重要陷阱:它会阻塞浏览器渲染!

因为useLayoutEffect执行期间,浏览器一直在等待,直到它执行完才能继续绘制。如果你在里面执行耗时操作,用户就会感觉页面卡住了。

来看个反面教材:

javascript 复制代码
function SlowComponent() {
  const [data, setData] = useState(null);

  useLayoutEffect(() => {
    // 错误示范:在useLayoutEffect中执行耗时操作
    const fetchData = async () => {
      // 模拟一个耗时的网络请求
      await new Promise(resolve => setTimeout(resolve, 2000));
      setData('加载完成的数据');
    };
    
    fetchData();
  }, []);

  return <div>{data || '加载中...'}</div>;
}

这段代码会让用户面对一个空白页面整整2秒钟!因为useLayoutEffect阻塞了渲染,直到数据获取完成后,浏览器才能绘制页面。

正确的做法是:

javascript 复制代码
function CorrectComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 正确:在useEffect中执行耗时操作
    const fetchData = async () => {
      await new Promise(resolve => setTimeout(resolve, 2000));
      setData('加载完成的数据');
    };
    
    fetchData();
  }, []);

  return <div>{data || '加载中...'}</div>;
}

这样至少用户能立即看到"加载中..."的提示,知道页面正在工作。

什么时候用哪个?一张图帮你决定

简单来说,记住这个原则:

用useLayoutEffect:当你需要直接操作DOM或避免视觉闪烁时 比如:测量元素尺寸、调整元素位置、同步更新状态以避免闪烁

用useEffect:当你需要执行副作用且不关心绘制时机时 比如:数据获取、订阅事件、手动更改文档标题

再分享一个实际开发中的经验:如果你不确定该用哪个,先试试useEffect。只有在发现视觉闪烁问题时,才考虑改用useLayoutEffect。

真实案例:自定义Tooltip组件

假设我们要实现一个自定义的Tooltip组件,需要根据目标元素的位置来计算Tooltip的显示位置。

用useEffect实现:

javascript 复制代码
function Tooltip({ children, content }) {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const targetRef = useRef(null);
  const tooltipRef = useRef(null);

  useEffect(() => {
    if (targetRef.current && tooltipRef.current) {
      const targetRect = targetRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      // 计算Tooltip位置,避免超出屏幕
      let top = targetRect.bottom + 5;
      let left = targetRect.left;
      
      // 如果Tooltip会超出右边界,调整位置
      if (left + tooltipRect.width > window.innerWidth) {
        left = window.innerWidth - tooltipRect.width - 5;
      }
      
      setPosition({ top, left });
    }
  }, [content]);

  return (
    <>
      <span ref={targetRef}>{children}</span>
      <div
        ref={tooltipRef}
        style={{
          position: 'fixed',
          top: position.top,
          left: position.left,
          display: content ? 'block' : 'none'
        }}
      >
        {content}
      </div>
    </>
  );
}

这个实现会有闪烁问题:Tooltip会先出现在默认位置(0,0),然后突然跳到正确位置。

改用useLayoutEffect:

javascript 复制代码
// 只需要把上面的useEffect换成useLayoutEffect
useLayoutEffect(() => {
  // ...同样的位置计算逻辑
}, [content]);

现在Tooltip就会直接出现在正确位置,没有任何闪烁!

总结一下

useEffect和useLayoutEffect就像两个性格不同的助手:

useEffect是"不紧不慢型",等浏览器画完页面再干活,适合不紧急的任务 useLayoutEffect是"雷厉风行型",必须在浏览器画画前把事情搞定,适合紧急的DOM操作

记住关键区别:useLayoutEffect会阻塞浏览器渲染,useEffect不会

现在你应该明白怎么选择了吧?下次遇到页面闪烁问题,就知道该请哪位"助手"出马了!

你在项目中还遇到过哪些关于useEffect和useLayoutEffect的坑?欢迎在评论区分享你的经历和解决方案~

相关推荐
艾小码2 小时前
告别Vue混入的坑!Composition API让我效率翻倍的3个秘密
前端·javascript·vue.js
骑自行车的码农2 小时前
【React用到的一些算法】游标和栈
算法·react.js
南雨北斗2 小时前
VS Code 中手动和直接运行TS代码
前端
小高0072 小时前
🔍说说对React的理解?有哪些特性?
前端·javascript·react.js
烛阴2 小时前
【TS 设计模式完全指南】懒加载、缓存与权限控制:代理模式在 TypeScript 中的三大妙用
javascript·设计模式·typescript
Samsong2 小时前
JavaScript逆向之反制无限debugger陷阱
前端·javascript
skykun2 小时前
今天你学会JS的类型转换了吗?
javascript
Lotzinfly2 小时前
8 个经过实战检验的 Promise 奇淫技巧你需要掌握😏😏😏
前端·javascript·面试