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。
相关推荐
瑞瑞小同学6 小时前
处理echarts x轴内容过多,重叠问题
前端·javascript·echarts
HexCIer6 小时前
面向未来的原子化 CSS:UnoCSS 核心架构分析与 Tailwind CSS 现状
前端·css·vite
程序员黑豆7 小时前
鸿蒙应用开发中的单位详解:px、vp、fp、lpx
前端·harmonyos
像我这样帅的人丶你还7 小时前
MCP + npm:给五年前的老系统接上AI
前端·javascript·agent
南一Nanyi7 小时前
依赖注入和控制反转
前端·设计模式·nestjs
小村儿7 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
愚公移码7 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
promiseThen8 小时前
5 分钟上手 Markdown:标题到表格、代码块与简历实战
前端
web66liang8 小时前
webpack4+vue2项目使用 sass-embedded 导致的 DockerfIle 构建失败的问题
前端
Strayer8 小时前
拓扑管网 3D 可视化大屏demo:科技感(地图 + 拓扑 + 3D 空间)
前端·three.js·数据可视化