布局抖动与强制同步布局

1. 核心概念

强制同步布局布局抖动 是导致页面在滚动或动画时发生卡顿的主要原因。

当我们用 JS 修改 DOM 元素的样式时,浏览器并不会立刻重排重绘,而是把这些修改放入队列中,等到下一帧再批量执行布局计算。

如果在修改样式的同一循环里,去读取某个几何样式属性(如 offsetHeightclientWidth),浏览器为了保证返回准确的数据,不得不立即暂停 JS 执行,强制提前清空修改队列,进行完整布局计算。这就是强制同步布局

如果这个“写入-读取”动作发生在一个循环里,会在极短时间引发大量布局计算,导致 CPU 占用率飙升,这种现象就是布局抖动

2. 触发属性

读取以下属性或调用以下方法会引发强制布局:

  • offsetWidth, offsetHeight, clientWidth, clientHeight, scrollWidth, scrollHeight
  • offsetTop, offsetLeft, scrollTop, scrollLeft
  • getBoundingClientRect(), getComputedStyle(), scrollIntoView()

3. 优化方案

(1) 读写分离

解决循环里布局抖动的核心做法是:先批量读取并缓存结果,再批量写入。

// 优化后代码:读写分离
function resizeAllParagraphsToMatchBlockWidth() {
  const paragraphs = document.querySelectorAll('p');
  const blocks = document.querySelectorAll('.block');

  // 1. 批量读取
  const widths = [];
  for (let i = 0; i < blocks.length; i++) {
    widths.push(blocks[i].offsetWidth); 
  }

  // 2. 批量写入
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = widths[i] + 'px';
  }
}

(2) 使用 requestAnimationFrame

可以利用 requestAnimationFrame 将写操作推迟到下一帧执行,从而保证当前帧只有读操作。也可以借助现成的库如 FastDOM 来统一调度读写队列。

// 读操作立即执行,写操作推迟到下一帧
let width = element.offsetWidth;
requestAnimationFrame(() => {
  otherElement.style.width = width + 'px';
});

(3) 优先使用 CSS 布局

尽量通过 Flexbox 或 Grid 等现代 CSS 布局特性来实现自适应,避免使用 JS 动态计算并设置宽高。