React 中 useEffect 和 useLayoutEffect 选择

FreeGuideOnline 最新 2026-07-07

React 中 useEffect 与 useLayoutEffect 的选择指南

在 React 函数组件中,useEffectuseLayoutEffect 是处理副作用的两个核心 Hook。它们签名完全一致,但执行时机有本质区别。错误的选择可能导致页面闪烁、布局跳动或性能问题。本教程将帮助你精准掌握两者的区别与最佳使用场景。

副作用 Hook 的共同基础

两者都用于在函数组件中执行副作用操作,如数据请求、DOM操作、订阅等。使用方式完全相同:

useEffect(setupFn, dependencies?);
useLayoutEffect(setupFn, dependencies?);
  • setupFn: 副作用执行函数,可选择返回一个清理函数。
  • dependencies: 依赖数组,控制何时重新执行。

先理解它们适用的共同规则,再分析差异。

useEffect:异步执行,不阻塞渲染

useEffect 在浏览器完成布局与绘制之后异步执行。React 确保先完成屏幕更新,再运行副作用。这意味着:

  • 不会阻塞视觉更新,应用保持响应。
  • 适合大多数与 DOM 不直接耦合的副作用。

典型执行流程

  1. React 提交组件树到 DOM。
  2. 浏览器进行布局计算,绘制像素到屏幕。
  3. 用户看到最新 UI。
  4. React 调用 useEffect 内的函数。

由于绘制完成后才执行,如果副作用修改了 DOM 并导致新的绘制,用户可能会先看到旧状态,然后很快看到更新后的状态,造成闪烁。因此,对于直接操作 DOM 或需要同步更新布局的场景,useEffect 不是最佳选择。

常见应用场景

  • 数据请求fetch 数据、更新 state,不会引起可见的布局变化。
  • 事件订阅: 如 window.addEventListener
  • 手动更改非关键 DOM: 例如修改 document.title、记录日志、分析统计。
  • 定时器、计时器setInterval, setTimeout

示例:数据获取

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        if (!ignore) setUser(data);
      });
    return () => { ignore = true; };
  }, [userId]);

  return <div>{user ? user.name : '加载中...'}</div>;
}

useLayoutEffect:同步执行,在绘制之前

useLayoutEffectuseEffect 本质区别在于:它会在 DOM 变更之后、浏览器绘制之前同步调用。换句话说,React 执行完 useLayoutEffect 内的代码后,浏览器才会将内容绘制到屏幕。

执行顺序:

  1. React 完成 DOM 变更(提交阶段)。
  2. React 立即同步执行 useLayoutEffect 内的函数。
  3. 浏览器进行布局计算并绘制界面。

用户看到的始终是 useLayoutEffect 执行后的最终状态,因此可以避免视觉上的闪烁。但这也意味着如果内部逻辑繁重,会拖慢浏览器绘制,导致交互卡顿。

典型应用场景

  • 需要立即读取布局信息并同步修改 DOM: 例如测量元素尺寸、位置,并基于测量结果更新样式。
  • 防止元素跳动或闪烁: 让 UI 在绘制前就处于最终状态。
  • 第三方库 DOM 操作: 如使用 scrollIntoView、手动管理过渡动画。
  • 当状态变化后必须同步阻止默认行为或执行同步 DOM 调整。

示例:避免模态框闪烁

假设在显示模态框前需要计算其位置,如果在 useEffect 中计算,用户会先看到模态框出现在错误位置,然后跳正。

import { useState, useLayoutEffect, useRef } from 'react';

function Modal() {
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const modalRef = useRef(null);
  const [open, setOpen] = useState(false);

  useLayoutEffect(() => {
    if (open && modalRef.current) {
      const { innerWidth, innerHeight } = window;
      const rect = modalRef.current.getBoundingClientRect();
      setPosition({
        top: (innerHeight - rect.height) / 2,
        left: (innerWidth - rect.width) / 2,
      });
    }
  }, [open]);

  return open ? (
    <div
      ref={modalRef}
      style={{ position: 'absolute', top: position.top, left: position.left }}
    >
      模态框内容
    </div>
  ) : null;
}

使用 useLayoutEffect 确保在浏览器绘制前已经调整好位置,用户不会看到未对齐的瞬间。

执行时机对比:记忆口诀

useEffect绘制后异步执行;
useLayoutEffect绘制前同步执行。

可以通过以下伪时间线理解:

【提交阶段】 -> DOM 更新
    ↓
【useLayoutEffect 同步】  <-- 此时页面还未绘制
    ↓
【浏览器 Layout + Paint】
    ↓
【useEffect 异步】       <-- 画面已显示给用户

选择决策树

根据副作用是否直接影响视觉布局来决定:

  • 副作用与视觉布局无关? ➔ 使用 useEffect
  • 需要测量 DOM 并同步应用修改,否则会出现可见的闪烁? ➔ 使用 useLayoutEffect
  • 不确定? 先用 useEffect,仅在出现可见的布局跳动时切换为 useLayoutEffect

注意:useLayoutEffect 内部代码会阻止浏览器绘制,如果包含大量计算或网络请求,会严重降低应用性能。务必保持其内部逻辑轻量、同步。

服务端渲染(SSR)注意事项

useLayoutEffect 在服务端渲染时不会执行,React 会给出警告。如果你的代码需要在服务端运行,要么改用 useEffect,要么使用条件判断保证只在客户端执行:

import { useEffect, useLayoutEffect } from 'react';

const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

这样在 SSR 环境下回退到 useEffect,避免错误。

清理函数的行为

两者的清理函数调用时机同样遵循上述规则:

  • useEffect 的清理函数在下一次副作用执行前或组件卸载时调用,且同样是异步的(不阻塞绘制)。
  • useLayoutEffect 的清理函数会同步执行,同样发生在重渲染的 DOM 变更之后、新副作用运行之前。

示例:同步清理事件监听器,避免内存泄漏。

useLayoutEffect(() => {
  const handleResize = () => {
    // 测量并更新布局...
  };
  window.addEventListener('resize', handleResize);
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

综合示例:实时高度测量

要求当内容变化时,容器能立即调整高度且无闪烁:

import { useState, useLayoutEffect, useRef } from 'react';

function AutoHeightPanel({ content }) {
  const panelRef = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    if (panelRef.current) {
      const newHeight = panelRef.current.scrollHeight;
      if (newHeight !== height) {
        setHeight(newHeight);
      }
    }
  }, [content, height]);

  return (
    <div ref={panelRef} style={{ height: height || 'auto', overflow: 'hidden' }}>
      {content}
    </div>
  );
}

如果换成 useEffect,高度变化会在绘制后执行,产生短暂的尺寸跳动。而 useLayoutEffect 让它即刻生效。

性能陷阱与最佳实践

  • 避免在 useLayoutEffect 中进行复杂计算: 保持同步代码简洁,如有耗时操作可以将其移到 useEffect 或 Web Worker 中。
  • 批量读取与写入 DOM: 尽量一次性读取所有需要的布局属性,然后统一写入,减少强制同步布局的触发次数。
  • 默认优先选用 useEffect: 绝大多数副作用都不需要影响首次绘制,使用 useEffect 能使应用性能更优。
  • 当 useState 与 useLayoutEffect 结合时,注意额外的重渲染: 内部调用 setState 会触发同步的重新提交,可能导致多次布局计算,谨慎使用。

总结速查表

特性 useEffect useLayoutEffect
执行阶段 绘制后 绘制前
调用方式 异步 同步
是否阻塞绘制
视觉闪烁风险 可能有 避免
适用场景 数据请求、订阅、日志 测量、同步 DOM 修改
性能影响 较小 可能阻塞渲染
SSR 兼容性 正常 需特殊处理

掌握这一区别后,你就能在 React 组件中精准安排副作用,让界面既流畅又稳定。