React useLayoutEffect vs useEffect:页面闪一下的元凶与正确修法

React useLayoutEffect vs useEffect:页面闪一下的元凶与正确修法

你写了个 Tooltip,根据触发元素的位置动态算坐标。跑起来功能没问题,但每次弹出时它会先在左上角闪现一下,再"跳"到正确位置。这种肉眼可见的抖动,十有八九是因为你用了 useEffect 而不是 useLayoutEffect

这两个 Hook 签名一模一样,90% 的场景可以互换,但剩下 10% 用错就会闪屏。这篇讲清它们的执行时机差异,以及什么时候必须换成 useLayoutEffect

核心区别:在浏览器绘制之前还是之后

React 更新 DOM 的大致流程是:执行组件函数 → 提交 DOM 变更 → 浏览器绘制到屏幕 → 用户看到画面。两个 Hook 的差别就在于相对「绘制」这一步的时机:

  • useEffect :在浏览器绘制之后异步执行。用户已经看到了这一帧,你的 effect 才跑。
  • useLayoutEffect :在 DOM 变更后、浏览器绘制之前同步执行。它跑完,用户才看到画面。

关键推论:如果你的 effect 里读取 DOM 布局(尺寸、位置)然后又修改 DOM ,用 useEffect 会让用户先看到「修改前」的一帧、再看到「修改后」的一帧------这就是闪屏。而 useLayoutEffect 在绘制前就改完了,用户只看到最终结果。

复现闪屏:一个定位 Tooltip

jsx 复制代码
import { useEffect, useRef, useState } from "react";

function Tooltip({ targetRef, children }) {
  const tipRef = useRef(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  useEffect(() => {
    // 读取触发元素的位置,算出 tooltip 该放哪
    const rect = targetRef.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 8, left: rect.left });
    // 问题:此时浏览器已经把 tooltip 画在了 {0,0},
    // setPos 触发重渲染后才移到正确位置 ------ 用户看到它"跳"了一下
  }, [targetRef]);

  return (
    <div ref={tipRef} style={{ position: "fixed", top: pos.top, left: pos.left }}>
      {children}
    </div>
  );
}

初始 pos{0,0},浏览器先把 tooltip 绘制在左上角,useEffect 绘制之后才跑、才 setPos、才移到正确位置。中间那一帧的左上角残影,就是用户看到的闪烁。

修法:换成 useLayoutEffect

只改一个词:

jsx 复制代码
import { useLayoutEffect, useRef, useState } from "react";

function Tooltip({ targetRef, children }) {
  const tipRef = useRef(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    const rect = targetRef.current.getBoundingClientRect();
    setPos({ top: rect.bottom + 8, left: rect.left });
    // useLayoutEffect 在绘制前同步执行,setPos 引发的重渲染也在绘制前完成,
    // 浏览器一次性画出最终位置 ------ 没有中间帧,不闪
  }, [targetRef]);

  return (
    <div ref={tipRef} style={{ position: "fixed", top: pos.top, left: pos.left }}>
      {children}
    </div>
  );
}

React 会在 useLayoutEffect 里的 setState 之后、绘制之前完成重渲染,所以用户直接看到正确位置。这就是它存在的意义:同步测量 + 修改 DOM,消除中间帧

那什么时候该用哪个?

判断很简单,问一句:这个 effect 会不会在绘制前读取布局并修改视觉结果?

useLayoutEffect 的典型场景(都涉及「测量后立即改样式」):

  • 读取元素尺寸/位置来定位(Tooltip、Popover、下拉菜单)。
  • 根据内容高度做动画的初始值(比如手风琴展开)。
  • 滚动位置的同步恢复(避免先跳到顶部再跳回)。

其余情况一律用 useEffect,包括:

jsx 复制代码
// 数据请求 ------ 和布局无关,用 useEffect
useEffect(() => {
  fetch("/api/data").then(/* ... */);
}, []);

// 订阅事件、埋点、定时器 ------ 都用 useEffect
useEffect(() => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);

别滥用:useLayoutEffect 会阻塞绘制

既然 useLayoutEffect 能防闪屏,为什么不全用它?因为它是同步、阻塞绘制的:浏览器必须等它(以及它引发的重渲染)跑完才能画下一帧。如果你在里面干重活(复杂计算、大量 DOM 操作),会直接拖慢首屏、造成卡顿。

原则:默认用 useEffect,只有出现闪屏、或明确需要绘制前测量 DOM 时,才换 useLayoutEffect 不要「为了保险」全部用它。

附带一个 SSR 警告

如果你用 Next.js 等服务端渲染,useLayoutEffect 在服务端会打印一条警告:「useLayoutEffect does nothing on the server」。因为服务端没有真实 DOM 可测量。处理方式:确认这段逻辑确实只需在客户端跑,可以用「客户端挂载后才渲染」的模式规避,或把纯副作用挪回 useEffect。不要为了消警告无脑改回 useEffect 而让闪屏复活------先判断它到底需不需要绘制前执行。

小结

  • useEffect 在浏览器绘制后 异步执行,useLayoutEffect绘制前同步执行。
  • 「读取 DOM 布局 → 修改视觉结果」的 effect 用 useEffect 会闪一帧,换 useLayoutEffect 消除中间帧。
  • 数据请求、订阅、定时器等和布局无关的副作用,一律用 useEffect
  • useLayoutEffect 阻塞绘制,别滥用;里面别干重活。
  • SSR 下 useLayoutEffect 会告警,按「是否真需要绘制前执行」来决定处理方式,而不是无脑改回去。
  • 一句话记忆点:画面闪一下、元素"跳"到最终位置,先想想那个 effect 是不是该从 useEffect 换成 useLayoutEffect。
相关推荐
FreeTinker5 小时前
HTML本地存储技术解析:localStorage与sessionStorage的原理、差异与应用场景
前端·html
qq_452396235 小时前
第十二篇:《全链路监控与可观测性:前端错误追踪与性能分析》
前端
海兰6 小时前
【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(三)
javascript·python·金融
wangruofeng6 小时前
Phosphor Icons 官网源码拆解:URL 即存储、水波动画与 7362 Star 图标库的架构实践
前端·架构·github
BigTopOne7 小时前
WebRTC Native / Android 开发学习资源整理
前端
excel7 小时前
nuxt 3 升级 nuxt 4 报错之 hasInjectionContext
前端
小磊哥er7 小时前
深入解构Claude Code - 第 7 篇 · 连接外面的世界
javascript·ai编程
何以解忧,唯有..8 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
小磊哥er8 小时前
深入解构Claude Code - 第 6 篇 · 终端里的图形界面
javascript·ai编程
海兰8 小时前
【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(一)
javascript·python·金融