← 中文资源

简体中文版本

布局抖动 — 滚动处理器强制同步重排

消除读写交错的布局抖动,用 DevTools 测量重排,并用 requestAnimationFrame 或 transform 批处理。

阅读英文原文

症状

  性能轨迹就是案发现场:长条紫色 Layout 与零碎 script 交替出现、每个滚动事件重复一轮;控制台补上自白——“Forced reflow while executing JavaScript took 42ms” 并指向触发读取的那一行。经典源码形态是循环里“读 top、写 transform、再读 top”:写使布局缓存失效,下一次读强制同步重算,每次迭代付一次完整布局,数百元素乘以每秒数十次事件。滚动卡顿、resize 期间掉帧,都是这个机制的直接体验。

诊断思路

  先录 Performance 看紫色 Layout 条与脚本片的交错形状;再开强制重排警告拿到触发行号;最后用代理计数 getBoundingClientRect 调用,量化单个滚动事件的读取放大倍数。用 top/left 做动画、框架渲染循环里边读 DOM 边改样式,是两个高频变体。

根因

  浏览器缓存布局并惰性重算。样式写入使缓存失效,下一次几何读取强制同步重算。读写交错让每个周期都付一次完整布局;叠加到每个滚动事件上,本来的缓存毫秒读变成撕碎帧率的工作。

修复与验证

  分相批处理:先完成全部读(数组缓存测量值),再执行全部写,两相之间互不穿插;事件驱动的工作用 requestAnimationFrame 节流到帧预算(配 passive 监听器);动画只走合成器路径——transform 与 opacity,top/left/width/height 按定义每帧都要付布局。验证时重录滚动轨迹:Layout 条缩成偶尔的小块、强制重排警告消失、帧率守住显示预算;若迁移到 transform 定位,记得复核遮罩层与层叠语义。

代码示例

[script 0.4ms] [Layout 18ms] [script 0.3ms] [Layout 17ms] ...
items.forEach((el) => {
  const top = el.offsetTop;        // read -> forces layout after...
  el.style.transform = `translateY(${top}px)`; // ...this write invalidated it
});
function update() {
  // read phase: no writes in between
  const tops = items.map((el) => el.offsetTop);

  // write phase: no reads in between
  items.forEach((el, i) => {
    el.style.transform = `translateY(${tops[i]}px)`;
  });
}
let queued = false;
window.addEventListener('scroll', () => {
  if (queued) return;
  queued = true;
  requestAnimationFrame(() => { update(); queued = false; });
}, { passive: true });