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

什么是布局抖动?

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

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

是什么导致了它?

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

  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/... 《🌊 布局抖动:是什么?如何消除它》

相关推荐
黑风风1 小时前
深入理解 Promise 和 Async/Await,并结合 Axios 实践
开发语言·前端·javascript
我命由我123451 小时前
Tailwind CSS 问题:npm error could not determine executable to run
前端·css·前端框架·npm·node.js·html·html5
PBitW1 小时前
阅读《Vue.js设计与实现》 -- 02
前端·vue.js·面试
浩男孩1 小时前
面试官提问:TypeScript 中的 Type 和 Interface 有什么区别?
前端·typescript
m0_582481492 小时前
qt作业day2
java·linux·前端
好想Z☡zᶻ2 小时前
调用的子组件中使用v-model绑定数据以及使用@调用方法
前端·javascript·vue.js
seven1082 小时前
cursor MCP server 如何AI 编程中实现动态数据获取
前端·cursor·mcp
予安灵2 小时前
《白帽子讲 Web 安全》之文件操作安全
前端·安全·web安全·系统安全·网络攻击模型·安全威胁分析·文件操作安全
m0_748244962 小时前
WebSpoon9.0(KETTLE的WEB版本)编译 + tomcatdocker部署 + 远程调试教程
前端
智绘前端3 小时前
sass语法@import将被放弃???升级@use食用指南!
前端·css·sass·scss