🌊 布局抖动:是什么?如何消除它

什么是布局抖动?

布局抖动,也称为强制同步回流,是指当浏览器被迫使放弃正在优化的布局,重新计算队列,并立即执行,因为正在运行的脚本需要知道最新的布局测量值。

由于布局重新计算和重绘是资源密集型操作,如果浏览器被迫过于频繁地执行这些操作,这会导致页面出现非常卡顿,甚至可能无响应。

是什么导致了它?

当用户与页面交互或动画播放时,浏览器需要不断重新渲染页面。这大致分为两个不同的步骤:

  1. 🌊 回流:重新计算页面上元素的位置、大小等。
  2. 🎨 重绘:重新绘制外观发生变化的元素。

回流和重绘的频率取决于屏幕刷新率。

理想情况下,每秒最多发生 60、75 次或更多次(即屏幕刷新率)。这是因为,回流和重绘相当耗费资源,如果执行频率超过这个范围,会导致性能卡顿。

然而,除非页面上的脚本专门针对这一点进行了优化,很可能在每个动画帧中会发生多次回流或重绘

通常情况下,浏览器会将它们推迟到下一次屏幕刷新之前。浏览器还很聪明地缓存布局信息,并通过跟踪自上次回流以来的变化,知道是否实际需要重新计算,即样式/布局是否已被"无效化"(invalidated)。但如果脚本在样式被无效化后读取,则浏览器被迫立即重新计算,以提供正确的测量值。

null

图片来源


因此,要发生强制同步回流,需要按顺序发生两件事

  1. 脚本以某种方式修改样式,使其无效化 ,例如添加 CSS 类或直接修改内联样式。从现在起,我将这称为"修改 "(mutation)。
  2. 然后请求测量 ,这取决于最新的样式,例如元素的高度。从现在起,我将这称为"测量 "(measurement)。

null

如何避免它?

避免强制回流的操作顺序是:

  1. 帧开始:浏览器运行优化的重新计算和重绘。
  2. JavaScript 可以自由读取或"测量 "任何布局值,例如 offsetHeightclientHeightgetBoundingClientRectgetComputedStyleevent.offsetX 等。有关"测量"操作的详细列表,如果这些操作在无效的布局上执行,将强制同步回流。
  3. ... 更多计算
  4. 帧结束:requestAnimationFrame 回调触发。
  5. 重复

为了实现这一点,一般原则是:

  1. 批量处理将所有样式修改推迟到帧结束之前
  2. 确保任何测量都在回流之后进行,并在样式被无效化之前完成。

步骤 1 很简单:任何需要修改样式的代码都应该在 requestAnimationFrame 的回调中运行。你可以自己实现,也可以使用类似 fastdom 的工具。

js 复制代码
// 步骤 1:将所有修改批量处理到帧结束时

requestAnimationFrame(() => {
  // ... 修改 DOM,例如
  element.classList.add("cls");
  element.append(newChild);
});

步骤 2 可能会有点棘手。如果网页上运行的所有代码都是你的,并且所有样式修改都按照步骤 1 中提到的进行批量处理,那么你可以在任何时候进行测量,除了在修改回调中(因为此时样式已被无效化,进行测量将强制回流)。

js 复制代码
// 步骤 2:更简单的天真方法
// 只需同步进行测量,
// 假设所有修改将推迟到帧结束时

// 希望此时样式没有被无效化
const elHeight = element.offsetHeight;

然而,在大多数情况下,页面上的代码并不全是你的 ,不会将修改推迟到帧结束时,因此当你进行测量时,如果它恰好在已被无效化的样式上运行,将强制回流。fastdom 采取了一种简单的方法,将所有测量(除了修改)也推迟到帧结束时,并先运行所有测量,然后运行所有修改。这确保了你的代码强制的回流在帧中最多发生一次:在 requestAnimationFrame 回调中运行的第一个测量任务期间。然后在帧结束时当然会有回流,因为样式已被修改无效化。

出于这个原因,我选择在我的最新项目中不使用 fastdom,而是编写了自己的实现:

  1. 批量处理并将修改推迟到帧结束时
  2. 批量处理并将测量推迟到下一帧开始后,此时样式几乎可以肯定是有效的,刚刚被重新计算。

现在,即使页面上的其他代码在帧期间修改了布局,我自己的代码也不会强制回流。

步骤 2 是通过首先等待 requestAnimationFrame 回调触发,然后使用 Scheduler.postTask 方法或 MessageChannel 来安排一个高优先级任务。这些方法安排了一个新任务(不是 微任务,而是宏任务),其优先级高于 setTimeout

这有效是因为从 requestAnimationFrame 回调中的任务将被推迟到任何的回流/重绘之后执行。

js 复制代码
// 步骤 2:更稳健的方法
// 将你的测量推迟到下一次计划的回流之后

const scheduleHighPriorityTask = (task) => {
  if (typeof scheduler !== "undefined") {
    scheduler.postTask(task, {
      priority: "user-blocking",
    });
  } else {
    // 回退到 MessageChannel
    const channel = new MessageChannel();
    channel.port1.onmessage = () => {
      channel.port1.close();
      task();
    };
    channel.port2.postMessage("");
  }
};

requestAnimationFrame(() => {
  // 这里是帧结束时,修改将运行/刚刚运行
  scheduleHighPriorityTask(() => {
    // 下一帧开始时:计划的回流刚刚发生
    const elHeight = element.offsetHeight;
  });
});

如何测试它?

浏览器内置的性能监控工具可以帮助你发现强制回流。

  1. 打开性能标签并开始录制
  2. 在页面上执行你怀疑会导致回流的操作
  3. 停止录制并检查时间线。强制回流将显示为顶部事件行下方的小紫色框。当你点击这些框时,它还会告诉你哪一行代码导致了这个问题:
null

总结

布局抖动是由于 JavaScript 之前修改了布局样式无效化了,浏览器缓存的测量值,现在 JavaScript 想要读取布局属性的最新值,导致浏览器被迫执行样式重新计算。

为了避免它:

  1. 使用 requestAnimationFrame 将所有修改 DOM 的代码批量处理并推迟到帧结束之前
  2. 使用 requestAnimationFrame 和高优先级任务调度器(如 Scheduler 或 MessageChannel)将所有测量批量处理并推迟到下一帧开始后

资源/参考

原文:www.yuque.com/fengjutian/... 《🌊 布局抖动:是什么?如何消除它》

相关推荐
小小小小宇8 分钟前
前端小tips
前端
小小小小宇17 分钟前
二维数组按顺时针螺旋顺序
前端
安木夕36 分钟前
C#-Visual Studio宇宙第一IDE使用实践
前端·c#·.net
努力敲代码呀~38 分钟前
前端高频面试题2:浏览器/计算机网络
前端·计算机网络·html
高山我梦口香糖1 小时前
[electron]预脚本不显示内联script
前端·javascript·electron
神探小白牙1 小时前
vue-video-player视频保活成功确无法推送问题
前端·vue.js·音视频
Angel_girl3192 小时前
vue项目使用svg图标
前端·vue.js
難釋懷2 小时前
vue 项目中常用的 2 个 Ajax 库
前端·vue.js·ajax
Qian Xiaoo2 小时前
Ajax入门
前端·ajax·okhttp
爱生活的苏苏2 小时前
vue生成二维码图片+文字说明
前端·vue.js