导读:本期聚焦于布兰登创作的《useLayoutEffect与useEffect有什么区别?一文搞懂React副作用钩子的执行时机》,敬请观看详情。为什么修改DOM后页面会闪烁?为什么官方不建议在服务端渲染中使用useLayoutEffect?这两个钩子名字相似,实际执行时机却完全不同:useEffect在浏览器绘制之后异步执行,而useLayoutEffect在DOM更新完成、浏览器绘制之前同步执行。本文从React的渲染流程讲起,详细对比两者的执行顺序、使用场景和性能差异,并结合读取布局信息、防闪烁动画、窗口滚动位置恢复等典型实例,给出清晰的选型建议,帮助你写出既流畅又不掉帧的React组件。

useEffect和useLayoutEffect是React中两个功能几乎一致的副作用钩子,连官方文档都直接用复制粘贴的方式介绍它们,唯一的区别在于一行小字:useLayoutEffect会在所有的DOM变更之后同步调用,可以在浏览器绘制之前执行。正是这一点点差异,决定了两个钩子完全不同的适用场景。用错了轻则页面闪烁,重则主线程被阻塞、交互卡顿。本文结合React的浏览器渲染流程,把这两个钩子的执行时机彻底讲清楚。

useLayoutEffect与useEffect有什么区别?一文搞懂React副作用钩子的执行时机

先理解浏览器一帧里发生了什么

要弄清楚两个钩子的区别,必须先知道浏览器渲染一帧的基本流程。React触发一次状态更新后,大致经历这几个阶段:组件函数执行(render阶段)、React计算差异并提交到真实DOM(commit阶段)、浏览器执行布局计算layout、最后把页面画到屏幕上paint。整个过程可以简化为:JS执行、样式计算、布局、绘制、合成。

useEffect被调度的位置在整个流程的最后面。React在commit阶段完成后,并不会立即执行useEffect的回调,而是把它交给调度器,以普通优先级异步安排执行。通常浏览器会先完成这一帧的绘制,再在稍后的空闲时间或下一次任务中执行effect回调。这意味着当useEffect的代码运行时,用户已经可能看到了更新前的旧界面。

useLayoutEffect则完全相反。它在commit阶段结束后被同步调用,此时DOM节点已经全部更新完毕,但浏览器还没有来得及计算布局和绘制。因此你在useLayoutEffect里读取offsetHeight、getBoundingClientRect这类布局属性,拿到的一定是最新值,而且在此处修改DOM或调用state更新,浏览器会把两次变更合并到同一次绘制中,用户完全感知不到中间状态。

执行顺序实验:一个组件里看清楚差异

下面这个例子可以直接粘贴到项目中运行,通过打印顺序观察两者的调用时机:

import React, { useState, useEffect, useLayoutEffect } from 'react';

export default function Demo() {
  const [count, setCount] = useState(0);

  // 打印顺序固定为:render -> layout -> effect
  useEffect(() => {
    console.log('useEffect 执行:浏览器已经绘制完成');
  });

  useLayoutEffect(() => {
    console.log('useLayoutEffect 执行:DOM已更新,还未绘制');
  });

  console.log('组件函数执行 render');

  return (
    <button onClick={() => setCount(count + 1)}>
      点击 {count}
    </button>
  );
}

每次点击按钮,控制台的输出顺序都是:组件函数执行、useLayoutEffect执行、useEffect执行。这验证了一个关键结论:useLayoutEffect总是先于useEffect执行,因为它发生在浏览器绘制之前,而useEffect发生在绘制之后。

更值得注意的是useLayoutEffect里的同步setState行为。如果你在useLayoutEffect中调用了setCount并改变了状态,React会在浏览器绘制之前同步完成重渲染,用户看到的是最终结果而不是中间状态。而同样的代码如果放在useEffect里,浏览器会先绘制一次旧状态,再因状态更新绘制第二次,页面上就出现了肉眼可见的闪烁。这也是useLayoutEffect存在的主要原因——解决两阶段渲染带来的视觉跳动问题。

典型使用场景对比

useLayoutEffect适合的场景

第一类是读取布局并同步修改DOM的场景。比如实现一个跟随鼠标的悬浮提示框,需要先测量触发元素的位置,再设置提示框的坐标。如果用useEffect,第一帧绘制时提示框位置是旧的,第二帧才修正,鼠标快速移动时会明显抖动。

function Tooltip({ targetRef, children }) {
  const ref = React.useRef(null);

  useLayoutEffect(() => {
    // DOM更新后立即测量,数据是最新的
    const rect = targetRef.current.getBoundingClientRect();
    const el = ref.current;
    // 直接操作样式,与本次DOM更新合并为同一次绘制
    el.style.left = rect.right + 8 + 'px';
    el.style.top = rect.top + 'px';
  });

  return <div ref={ref} style={{ position: 'fixed' }}>{children}</div>;
}

第二类是动画初始状态防闪烁。元素需要从某个测量好的位置开始做过渡动画,初始位置必须在绘制前确定。第三类是滚动位置恢复:列表数据刷新后要还原用户之前的滚动距离,如果放在useEffect里执行,用户会先看到列表回到顶部再跳回原位置。

useEffect适合的场景

绝大多数副作用都应该用useEffect:数据请求、订阅事件、定时器、日志上报、操作localStorage等。这些操作既不依赖布局信息,也不需要在绘制前完成,放在同步阶段反而会阻塞绘制,拖慢首屏呈现。数据请求尤其如此,接口响应本来就是异步的,用useLayoutEffect没有任何收益。

换一个角度看,useLayoutEffect是同步阻塞的,回调执行时间过长会直接推迟浏览器的绘制,造成掉帧。而useEffect是异步的,不影响本次渲染的呈现速度。所以社区有一个经验法则:默认写useEffect,只有在出现闪屏、布局抖动等视觉问题时,才切换到useLayoutEffect。这是正确顺序,不要反过来。

服务端渲染中的警告与兼容方案

在服务端渲染场景下,useLayoutEffect会触发一个著名的警告:useLayoutEffect does nothing on the Server。原因很直接,服务端不存在DOM和浏览器绘制流程,这个钩子的核心语义无从谈起。React为了保持行为一致性,直接在SSR环境下禁用了它。

如果组件确实需要useLayoutEffect,又要支持SSR,常用方案是用typeof window !== 'undefined'判断动态选择钩子,或者借助useIsomorphicLayoutEffect这类社区封装。代码示意如下:

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

这样在浏览器端走useLayoutEffect保证绘制前执行,在服务端退化为useEffect避免报错。此外还要注意,React 18的并发特性下,useEffect的触发时机由调度器决定,不同优先级的更新可能让effect延后执行;而useLayoutEffect始终紧跟commit同步执行,这个语义在任何版本中都是稳定的。

选型总结

一句话概括两者的关系:useLayoutEffect是浏览器绘制前的同步钩子,useEffect是绘制之后的异步钩子。判断标准很简单,问自己两个问题。第一,effect里的逻辑是否需要读取布局信息或者写DOM样式?第二,这步操作是否必须在用户看到画面之前完成?两个问题都是肯定答案才用useLayoutEffect,其余情况一律useEffect。记住这个决策路径,就能在保持页面流畅的同时彻底消除那些莫名其妙的闪烁问题。

useLayoutEffectuseEffectReact hooks修改时间:2026-09-12 23:00:33

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260912/55606.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。