React useLayoutEffect 详解:避免视觉闪烁的关键 Hook

88 阅读3分钟

概述

在 React 开发中,useLayoutEffect 是一个经常被误解但功能强大的 Hook。本文将通过实际项目示例深入解析 useLayoutEffect 的工作原理、使用场景以及与 useEffect 的关键区别。

什么是 useLayoutEffect?

useLayoutEffect 是 React 提供的一个副作用 Hook,它在所有 DOM 变更后同步执行,但在浏览器执行绘制之前完成。这个时机特性使得它成为解决视觉闪烁问题的关键工具。

基本语法

import { useLayoutEffect } from "react";

useLayoutEffect(() => {
  // 副作用代码
  return () => {
    // 清理代码(可选)
  };
}, [dependencies]);

useLayoutEffect vs useEffect:关键区别

特性useEffectuseLayoutEffect
执行时机异步,浏览器绘制后同步,浏览器绘制前
性能影响不阻塞渲染可能阻塞渲染
适用场景一般副作用操作DOM 测量、样式修改
用户体验可能产生闪烁避免视觉闪烁

执行时序图

组件渲染 → DOM更新 → useLayoutEffect执行 → 浏览器绘制 → useEffect执行

项目示例分析

示例 1:DOM 元素尺寸获取

function App() {
  const boxRef = useRef();

  console.log("渲染时的 ref.current:", boxRef.current); // null

  useEffect(() => {
    // useEffect 在浏览器绘制后执行,DOM已渲染完成
    if (boxRef.current) {
      console.log("useEffect height:", boxRef.current.offsetHeight);
    }
  }, []);

  useLayoutEffect(() => {
    // useLayoutEffect 在浏览器绘制前执行,能立即获取DOM信息
    console.log("useLayoutEffect height", boxRef.current.offsetHeight);
  }, []);

  return <div ref={boxRef} style={{ height: 100 }}></div>;
}

关键点分析:

  • 在组件渲染过程中,ref.currentnull
  • useLayoutEffect 能在绘制前同步获取 DOM 元素的准确尺寸
  • useEffect 在绘制后异步执行,此时 DOM 已完全渲染

示例 2:防止内容更新闪烁

function App() {
  const [content, setContent] = useState("6666666666666");
  const ref = useRef();

  // 使用 useEffect 可能产生闪烁
  useEffect(() => {
    setContent("很长的文本内容...");
    ref.current.style.height = "200px";
  }, []);

  // 使用 useLayoutEffect 避免闪烁
  useLayoutEffect(() => {
    setContent("很长的文本内容...");
  }, []);

  return (
    <div ref={ref} style={{ height: "50px", background: "lightblue" }}>
      {content}
    </div>
  );
}

关键点分析:

  • useEffect 版本:用户会先看到初始内容,然后看到内容突然变化(闪烁)
  • useLayoutEffect 版本:内容更新在绘制前完成,用户看到的是最终结果

示例 3:Modal 弹窗居中定位(当前活跃示例)

function Modal() {
  const ref = useRef();

  useLayoutEffect(() => {
    const height = ref.current.offsetHeight;
    ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`;
  }, []);

  return (
    <div
      ref={ref}
      style={{ position: "absolute", width: "200px", background: "red" }}
    >
      我是弹窗
    </div>
  );
}

实现原理:

  1. React 渲染 DOM 元素
  2. useLayoutEffect 同步执行,计算元素高度
  3. 动态设置 marginTop 实现垂直居中
  4. 浏览器绘制最终结果

为什么不用 useEffect? 如果使用 useEffect,用户会看到:

  1. 弹窗首先出现在顶部
  2. 然后"跳跃"到中央位置
  3. 产生明显的视觉闪烁

使用场景

适合使用 useLayoutEffect 的情况:

  1. DOM 测量和布局计算

    useLayoutEffect(() => {
      const { width, height } = element.getBoundingClientRect();
      // 基于测量结果调整布局
    }, []);
    
  2. 样式的同步修改

    useLayoutEffect(() => {
      element.style.transform = `translateX(${position}px)`;
    }, [position]);
    
  3. 滚动位置的精确控制

    useLayoutEffect(() => {
      scrollContainer.scrollTop = targetPosition;
    }, [targetPosition]);
    
  4. 动画初始状态设置

    useLayoutEffect(() => {
      element.style.opacity = "0";
      element.style.transform = "scale(0)";
    }, []);
    

继续使用 useEffect 的情况:

  1. 数据请求
  2. 事件监听器设置
  3. 定时器操作
  4. 不影响布局的副作用

性能考虑

优点:

  • 消除视觉闪烁
  • 提供同步的 DOM 访问
  • 确保布局计算的准确性

注意事项:

  • 同步执行可能阻塞渲染:复杂计算应避免
  • 谨慎使用:不是所有场景都需要
  • 性能监控:监控渲染性能,避免过度使用

最佳实践

1. 选择合适的 Hook

// ❌ 错误:简单的异步操作使用 useLayoutEffect
useLayoutEffect(() => {
  fetch("/api/data").then(setData);
}, []);

// ✅ 正确:使用 useEffect
useEffect(() => {
  fetch("/api/data").then(setData);
}, []);

// ✅ 正确:需要同步DOM操作使用 useLayoutEffect
useLayoutEffect(() => {
  const width = element.offsetWidth;
  element.style.left = `${width / 2}px`;
}, []);

2. 避免复杂计算

// ❌ 避免在 useLayoutEffect 中进行复杂计算
useLayoutEffect(() => {
  const result = heavyComputation(); // 阻塞渲染
  element.style.width = `${result}px`;
}, []);

// ✅ 将计算移到 useEffect 或 useMemo
const computedWidth = useMemo(() => heavyComputation(), [dependencies]);
useLayoutEffect(() => {
  element.style.width = `${computedWidth}px`;
}, [computedWidth]);

3. 合理使用依赖数组

useLayoutEffect(() => {
  // 确保依赖数组包含所有相关变量
  updateElementPosition(x, y);
}, [x, y]); // 明确列出依赖

调试技巧

1. 添加性能标记

useLayoutEffect(() => {
  performance.mark("layout-effect-start");
  // DOM操作
  performance.mark("layout-effect-end");
  performance.measure(
    "layout-effect",
    "layout-effect-start",
    "layout-effect-end"
  );
}, []);

2. 可视化渲染过程

useLayoutEffect(() => {
  console.log("Layout effect:", element.getBoundingClientRect());
}, []);

useEffect(() => {
  console.log("Effect after paint:", element.getBoundingClientRect());
}, []);

总结

useLayoutEffect 是解决 React 应用中视觉闪烁问题的重要工具。通过在浏览器绘制前同步执行,它确保了 DOM 操作和样式修改的时机准确性。

记住关键原则:

  • 需要同步 DOM 操作时使用 useLayoutEffect
  • 一般副作用操作继续使用 useEffect
  • 注意性能影响,避免复杂计算
  • 优先考虑用户体验,防止视觉闪烁

通过合理使用 useLayoutEffect,我们可以创建更加流畅、无闪烁的用户界面,提升整体的用户体验。