hook:useLayoutEffect
- 用法与
useEffect完全相同,但执行时机不同 - 核心区别: useLayoutEffect 在 DOM 变更后、浏览器绘制之前同步执行,而 useEffect 在绘制之后异步执行
执行时机对比
渲染流程:
1. React 更新 DOM
2. 🔵 useLayoutEffect 执行(同步,阻塞渲染)
3. 浏览器绘制屏幕
4. 🟢 useEffect 执行(异步,不阻塞渲染)
| Hook | 执行时机 | 是否阻塞渲染 | 用户是否看到闪烁 |
|---|---|---|---|
| useLayoutEffect | DOM 更新后,绘制前 | ✅ 阻塞 | ❌ 不会 |
| useEffect | DOM 更新后,绘制后 | ❌ 不阻塞 | ✅ 可能会 |
场景代码示例
-
【场景 1】测量 DOM 尺寸(常用):
-
需要在渲染前获取 DOM 尺寸,避免闪烁
❌ 使用 useEffect - 会闪烁
import { useState, useEffect, useRef } from "react"; const Tooltip = ({ text, children }) => { const [tooltipWidth, setTooltipWidth] = useState(0); const tooltipRef = useRef(); // ❌ useEffect:绘制后执行,用户会看到位置跳动 useEffect(() => { const width = tooltipRef.current.offsetWidth; setTooltipWidth(width); }, []); return ( <div> {children} <div ref={tooltipRef} style={{ position: "absolute", left: tooltipWidth > 100 ? "-50px" : "0px", // 位置会跳动 }} > {text} </div> </div> ); };✅ 使用 useLayoutEffect - 不会闪烁
import { useState, useLayoutEffect, useRef } from "react"; const Tooltip = ({ text, children }) => { const [tooltipWidth, setTooltipWidth] = useState(0); const tooltipRef = useRef(); // ✅ useLayoutEffect:绘制前执行,用户看到的是最终位置 useLayoutEffect(() => { const width = tooltipRef.current.offsetWidth; setTooltipWidth(width); }, []); return ( <div> {children} <div ref={tooltipRef} style={{ position: "absolute", left: tooltipWidth > 100 ? "-50px" : "0px", // 位置不会跳动 }} > {text} </div> </div> ); };
-
-
【场景 2】操作 DOM 避免闪烁(常用):
-
需要在渲染前修改 DOM,避免视觉闪烁
import { useLayoutEffect, useRef } from "react"; const ScrollToTop = ({ children }) => { const containerRef = useRef(); // ✅ useLayoutEffect:在绘制前滚动到顶部,用户看不到滚动过程 useLayoutEffect(() => { containerRef.current.scrollTop = 0; }, [children]); // children 变化时滚动到顶部 return ( <div ref={containerRef} style={{ height: "500px", overflow: "auto" }}> {children} </div> ); };对比 useEffect:
// ❌ useEffect:用户会看到先滚动到底部,再跳回顶部的闪烁 useEffect(() => { containerRef.current.scrollTop = 0; }, [children]);
-
-
【场景 3】同步动画状态(常用):
-
需要在渲染前同步动画相关的状态
import { useState, useLayoutEffect, useRef } from "react"; const AnimatedBox = ({ isOpen }) => { const [height, setHeight] = useState(0); const contentRef = useRef(); // ✅ useLayoutEffect:在绘制前计算高度,动画流畅 useLayoutEffect(() => { if (isOpen) { const contentHeight = contentRef.current.scrollHeight; setHeight(contentHeight); } else { setHeight(0); } }, [isOpen]); return ( <div style={{ height: `${height}px`, overflow: "hidden", transition: "height 0.3s ease", }} > <div ref={contentRef}> <p>这是一段内容</p> <p>可以折叠和展开</p> </div> </div> ); };
-
-
【场景 4】读取布局信息后更新(常用):
-
基于 DOM 布局信息更新状态
import { useState, useLayoutEffect, useRef } from "react"; const ResponsiveComponent = () => { const [isMobile, setIsMobile] = useState(false); const containerRef = useRef(); // ✅ useLayoutEffect:在绘制前判断是否移动端,避免布局跳动 useLayoutEffect(() => { const updateLayout = () => { const width = containerRef.current.offsetWidth; setIsMobile(width < 768); }; updateLayout(); window.addEventListener("resize", updateLayout); return () => window.removeEventListener("resize", updateLayout); }, []); return ( <div ref={containerRef}> {isMobile ? <MobileView /> : <DesktopView />} </div> ); };
-
-
【场景 5】第三方库需要同步初始化(进阶):
-
某些第三方库需要在 DOM 完全准备好后立即初始化
import { useLayoutEffect, useRef } from "react"; import Chart from "chart.js"; // 假设是图表库 const ChartComponent = ({ data }) => { const canvasRef = useRef(); const chartRef = useRef(); // ✅ useLayoutEffect:确保图表在绘制前完成初始化 useLayoutEffect(() => { const ctx = canvasRef.current.getContext("2d"); chartRef.current = new Chart(ctx, { type: "bar", data: data, }); return () => { chartRef.current.destroy(); }; }, [data]); return <canvas ref={canvasRef} />; };
-
useLayoutEffect vs useEffect 详细对比
| 特性 | useLayoutEffect | useEffect |
|---|---|---|
| 执行时机 | DOM 更新后,浏览器绘制前 | 浏览器绘制后 |
| 是否同步 | ✅ 同步执行 | ❌ 异步执行 |
| 是否阻塞渲染 | ✅ 阻塞浏览器绘制 | ❌ 不阻塞 |
| 用户体验 | 无闪烁,但可能卡顿 | 可能闪烁,但不卡顿 |
| 适用场景 | 需要读取布局、修改 DOM 避免闪烁 | 大部分副作用(网络请求、订阅等) |
| 性能 | 较差(阻塞渲染) | 较好(不阻塞渲染) |
| 推荐使用频率 | 少用 | 常用 |
| SSR 兼容性 | ⚠️ 服务端会警告 | ✅ 无问题 |
何时使用 useLayoutEffect
| 情况 | 说明 | 示例 |
|---|---|---|
| 需要测量 DOM 尺寸 | 获取元素宽高、位置等布局信息 | Tooltip 定位、响应式布局 |
| 需要在渲染前修改 DOM | 避免用户看到中间状态 | 滚动位置、元素样式 |
| 需要同步读取布局后更新 | 基于布局信息立即更新状态 | 自适应组件、折叠面板 |
| 需要避免视觉闪烁 | DOM 更新后立即执行,用户看不到变化 | 动画初始化、位置调整 |
| 第三方库需要同步初始化 | 某些库必须在 DOM 准备好后立即初始化 | 图表库、编辑器 |
性能对比示例
import { useState, useEffect, useLayoutEffect } from "react";
const PerformanceTest = () => {
const [count, setCount] = useState(0);
// useEffect:不阻塞渲染,用户体验更流畅
useEffect(() => {
// 模拟耗时操作
const start = Date.now();
while (Date.now() - start < 100) {} // 阻塞 100ms
console.log("useEffect 执行完毕");
}, [count]);
// useLayoutEffect:阻塞渲染,用户会感觉卡顿
useLayoutEffect(() => {
// 模拟耗时操作
const start = Date.now();
while (Date.now() - start < 100) {} // 阻塞 100ms
console.log("useLayoutEffect 执行完毕");
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
// 结果:
// - useEffect:点击按钮后,count 立即更新显示,100ms 后打印日志(流畅)
// - useLayoutEffect:点击按钮后,等待 100ms,然后 count 才显示(卡顿)
注意事项
-
⚠️ 优先使用 useEffect
// 默认情况下,使用 useEffect // 只有在确实需要同步执行时,才使用 useLayoutEffect -
⚠️ 服务端渲染 (SSR) 警告
// useLayoutEffect 在服务端会产生警告,因为服务端没有 DOM // 如果需要兼容 SSR,可以这样处理: import { useEffect, useLayoutEffect } from "react"; const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; // 使用 useIsomorphicLayoutEffect(() => { // 代码 }, []); -
⚠️ 避免耗时操作
// ❌ 错误:useLayoutEffect 中执行耗时操作会阻塞渲染 useLayoutEffect(() => { // 耗时计算 for (let i = 0; i < 1000000; i++) { // ... } }, []); // ✅ 正确:耗时操作放到 useEffect useEffect(() => { // 耗时计算 for (let i = 0; i < 1000000; i++) { // ... } }, []); -
⚠️ 不要过度优化
// 大部分情况下,用户不会注意到 useEffect 的闪烁 // 只有在确实出现视觉问题时,才切换到 useLayoutEffect -
✅ 测量 DOM 时使用
// ✅ 适合使用 useLayoutEffect 的典型场景 useLayoutEffect(() => { const rect = elementRef.current.getBoundingClientRect(); setPosition(rect); }, []); -
✅ 调试技巧
// 如果不确定是否需要 useLayoutEffect,可以先用 useEffect // 如果出现闪烁或布局跳动,再改为 useLayoutEffect // 调试时可以打印执行顺序 useLayoutEffect(() => { console.log("useLayoutEffect - 在绘制前执行"); }); useEffect(() => { console.log("useEffect - 在绘制后执行"); });
快速决策树
需要执行副作用?
↓
是否涉及 DOM 测量或修改?
↓ 否 → 使用 useEffect ✅
↓ 是
↓
用户是否会看到闪烁或布局跳动?
↓ 否 → 使用 useEffect ✅
↓ 是
↓
使用 useLayoutEffect ✅
总结
| 原则 | 说明 |
|---|---|
| 默认使用 useEffect | 99% 的情况都应该用 useEffect |
| 闪烁时用 useLayoutEffect | 只有在出现视觉问题时才切换 |
| 避免耗时操作 | useLayoutEffect 会阻塞渲染 |
| 测量布局用 useLayoutEffect | 获取尺寸、位置等布局信息时使用 |
| SSR 需要特殊处理 | 使用 useIsomorphicLayoutEffect |
记住:useLayoutEffect 是 useEffect 的同步版本,用于需要在浏览器绘制前执行的场景。