You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React useLayoutEffect阻塞UI更新的实现机制与现象解析

问题复现

运行下述代码,点击带绿色边框的Click me区域后,打开浏览器开发者工具的Elements标签页观察:可以看到class为msg的span元素已经被添加到DOM结构中,但页面要卡顿1秒后才会显示出红色的提示元素。

const sleep = (duration) => {
  const start = new Date().getTime();
  let end = start;

  while(end < start + duration) {
    end = new Date().getTime();
  }
}

const Message = ({boxRef, children}) => {
  const msgRef = React.useRef(null);

  React.useLayoutEffect(() => {
    const element = document.querySelector('.msg');
    console.log(element);
    const rect = boxRef.current.getBoundingClientRect();
    sleep(1000);
    msgRef.current.style.top = `${rect.height + rect.top}px`;
  }, []);

  return <span ref={msgRef} className="msg">{children}</span>;
};

const Index = () => {
  const [show, setShow] = React.useState(false);
  const boxRef = React.useRef(null);

  return (
    <div>
      <div ref={boxRef} className="box" onClick={() => setShow(prev => !prev)}>Click me</div>
      {show && <Message boxRef={boxRef}>Foo bar baz</Message>}
    </div>
  );
};

ReactDOM.render(<Index />, document.getElementById('root'));
.box {
  position: absolute;
  width: 100px;
  height: 100px;
  border: 1px solid green;
  user-select: none;
}

.msg {
  user-select: none;
  position: relative;
  border: 1px solid red;
  background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
原理说明

DOM已更新但页面未渲染的原因

这个效果没有什么特殊API,完全是React对useLayoutEffect的执行时机设计,刚好卡中了浏览器原生的渲染管线规则:

  • React对useLayoutEffect的执行时机定义非常明确:所有DOM修改完成后、浏览器执行视觉绘制之前,同步执行对应的回调函数
  • 浏览器本身的渲染逻辑是固定的:先执行当前任务队列里的JS代码,等JS全部执行完、主线程空闲后,才会走样式计算、布局、绘制的全流程,把最新的DOM结构渲染到用户可见的页面上

整个流程的执行顺序是:

  • 点击按钮触发状态更新,show变为true,React启动重渲染
  • React计算完虚拟DOM差异,把真实的span.msg节点插入到DOM树中,这一步结束时DOM结构已经变了,但浏览器还没到绘制环节
  • React立刻同步执行Message组件挂载阶段的useLayoutEffect回调
  • 回调里的sleep是靠while循环写的同步阻塞逻辑,会一直占住主线程1秒,这期间浏览器拿不到主线程控制权,根本没法走后续的绘制流程,所以用户看不到页面上出现新元素
  • 等1秒阻塞结束,useLayoutEffect回调执行完,React把主线程交还给浏览器,浏览器才走完剩下的渲染步骤,把红色提示框绘制到页面上

移除console.log后Elements面板看不到即时节点的原因

这个现象和React没关系,是Chrome DevTools本身的更新机制导致的:

  • Elements面板不会在DOM每次发生修改时都同步刷新视图,它的DOM树展示需要做DOM快照同步,这个操作要占用主线程时间片,跟着浏览器的渲染帧调度走
  • 保留console.log(element)时,控制台打印DOM元素是特殊的实时引用逻辑,会强制DevTools立刻同步当前DOM状态生成快照,所以你能第一时间在Elements里看到刚插入的节点
  • 删掉这句console之后,从DOM插入到sleep阻塞主线程的1秒里,没有任何操作触发DevTools做快照同步,主线程又被while循环完全占死,DevTools根本没机会执行刷新面板的逻辑,自然要等1秒阻塞结束、主线程释放后,才能在Elements里看到新增的节点。

补充:如果把阻塞逻辑挪到useEffect里就不会出现这个现象,因为useEffect是在浏览器完成页面绘制之后才会异步执行,元素会先显示到页面上,再触发1秒的卡顿。

内容的提问来源于stack exchange,提问作者Alexander Pankin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 16:57:20