useEffect 和 useLayoutEffect 的区别:别背定义,按"什么时候上屏"来选
以前一直写vue,现在写react了,写react代码的时候有时候会碰到一个选择题:
- 这个副作用用
useEffect还是useLayoutEffect? - 为什么我用
useEffect量 DOM 会闪一下? - Next.js 里
useLayoutEffect为什么会给我一个 warning?
这俩 Hook 的差别,说穿了就一句:它们跑在"上屏(paint)"的前后。
一句话结论(先拿走)
- 默认用
useEffect:不会挡住浏览器绘制。 - 只有在"必须读布局/写布局且不能闪"的时候用
useLayoutEffect:它会在浏览器 paint 之前同步执行。
如果你脑子里只留两句话,就留这两句。
它们到底差在哪:在浏览器 paint 的前后
把 React DOM 的一次更新粗暴拆成四步,你就不会混了:
useLayoutEffect:DOM 已经变了,但还没 paint。它会阻塞本次 paint。useEffect:页面已经 paint 了。它不会阻塞上屏(但也意味着你在里面改布局可能会"先错后改",肉眼看到就是闪)。
注意我在说的是"commit 后"的那个时间点,不是 render 阶段。
一个很真实的例子:测量 DOM 决定位置(useEffect 会闪)
比如你做一个 Tooltip:初始不知道自己宽高,得先 render 出来,然后用 getBoundingClientRect() 量一下,再把位置修正。
如果你用 useEffect:
- 第一次 paint:Tooltip 先用默认位置上屏
- effect 里量完 -> setState
- 第二次 paint:位置修正
用户看到的就是"闪一下"。如果你用 useLayoutEffect,修正发生在 paint 之前,第一帧就是对的。
下面这段代码可以直接在 React DOM 里跑(为了不违反 Hooks 规则,我写成两个组件,用 checkbox 切换时会 remount):
tsx
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
type TooltipPosition = {
anchorRef: React.RefObject<HTMLButtonElement | null>;
tipRef: React.RefObject<HTMLDivElement | null>;
left: number;
};
function calcLeft(anchor: HTMLButtonElement, tip: HTMLDivElement) {
const a = anchor.getBoundingClientRect();
const t = tip.getBoundingClientRect();
return Math.round(a.left + a.width / 2 - t.width / 2);
}
function useTooltipPositionWithEffect(): TooltipPosition {
const anchorRef = useRef<HTMLButtonElement | null>(null);
const tipRef = useRef<HTMLDivElement | null>(null);
const [left, setLeft] = useState(0);
useEffect(() => {
const anchor = anchorRef.current;
const tip = tipRef.current;
if (!anchor || !tip) return;
setLeft(calcLeft(anchor, tip));
}, []);
return { anchorRef, tipRef, left };
}
function useTooltipPositionWithLayoutEffect(): TooltipPosition {
const anchorRef = useRef<HTMLButtonElement | null>(null);
const tipRef = useRef<HTMLDivElement | null>(null);
const [left, setLeft] = useState(0);
useLayoutEffect(() => {
const anchor = anchorRef.current;
const tip = tipRef.current;
if (!anchor || !tip) return;
setLeft(calcLeft(anchor, tip));
}, []);
return { anchorRef, tipRef, left };
}
function TooltipFrame({ pos }: { pos: TooltipPosition }) {
return (
<>
<button ref={pos.anchorRef} style={{ marginLeft: 120 }}>
Hover me
</button>
<div
ref={pos.tipRef}
style={{
position: "fixed",
top: 80,
left: pos.left,
padding: "8px 10px",
borderRadius: 8,
background: "#111827",
color: "#fff",
fontSize: 12,
whiteSpace: "nowrap",
}}
>
I am a tooltip
</div>
</>
);
}
function DemoUseEffect() {
return <TooltipFrame pos={useTooltipPositionWithEffect()} />;
}
function DemoUseLayoutEffect() {
return <TooltipFrame pos={useTooltipPositionWithLayoutEffect()} />;
}
export function Demo() {
const [layout, setLayout] = useState(false);
return (
<div style={{ padding: 40 }}>
<label style={{ display: "block", marginBottom: 12 }}>
<input
type="checkbox"
checked={layout}
onChange={(e) => setLayout(e.target.checked)}
/>{" "}
用 useLayoutEffect(勾上后更不容易闪)
</label>
{layout ? <DemoUseLayoutEffect /> : <DemoUseEffect />}
</div>
);
}
真实项目里你可能还会处理 resize、内容变化(ResizeObserver)、字体加载导致的宽度变化等;但对理解这两个 Hook 的差别,上面这个例子够用了。
怎么选:我自己用的"决策口诀"
1)只要不读/写布局,就用 useEffect
典型场景:
- 请求数据、上报埋点
- 订阅/取消订阅(WebSocket、EventEmitter)
document.title、localStorage同步- 给 window/document 绑事件
这些东西不需要卡在 paint 之前完成,useEffect 更合适。
2)你要读布局(layout read)并且会影响第一帧渲染,就用 useLayoutEffect
典型场景:
getBoundingClientRect()/offsetWidth/scrollHeight这种- 计算初始滚动位置、同步滚动
- 需要避免视觉抖动的"测量 -> setState"
- focus / selection(输入框聚焦、光标定位)对首帧体验敏感
一句话:"不想让用户看到中间态"。
3)别在 useLayoutEffect 里干重活
因为它会阻塞 paint:
- 你在里面做重计算,页面就掉帧
- 你在里面频繁 setState,可能放大卡顿
如果你只是"想早点跑一下",但并不依赖布局,别用它。
Next.js / SSR 里那个 warning 怎么回事
在服务端渲染(SSR)时:
useEffect本来就不会执行(它只在浏览器跑)useLayoutEffect也不会执行,但 React 会提示你:它在服务端没意义,可能导致你写出"依赖布局但 SSR 不存在布局"的代码
如果你写的是"浏览器才有意义的 layout effect",又不想看到 warning,常见做法是包一层:
ts
import { useEffect, useLayoutEffect } from "react";
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
然后把需要 layout 的地方用 useIsomorphicLayoutEffect。
容易踩的坑(顺手说两句)
- Strict Mode 下 effect 会在开发环境额外执行一次 :
useEffect和useLayoutEffect都一样,别拿这个现象判断线上行为。 - "我在
useEffect里 setState 为什么会闪?":因为你改的是布局相关内容,第一帧已经 paint 了。 - 不要把数据请求塞进
useLayoutEffect:它既不需要 paint 前完成,还可能拖慢上屏。
简单总结一下
useEffect:大多数副作用的默认选择。useLayoutEffect:只在"必须卡在 paint 前解决"的那一小撮场景里用。
真要说区别,其实就是一句:你愿不愿意为了"第一帧正确"去挡住 paint。
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB