什么是布局抖动?
布局抖动,也称为强制同步回流,是指当浏览器被迫使放弃正在优化的布局,重新计算队列,并立即执行,因为正在运行的脚本需要知道最新的布局测量值。
由于布局重新计算和重绘是资源密集型操作,如果浏览器被迫过于频繁地执行这些操作,这会导致页面出现非常卡顿,甚至可能无响应。
是什么导致了它?
当用户与页面交互或动画播放时,浏览器需要不断重新渲染页面。这大致分为两个不同的步骤:
- 🌊 回流:重新计算页面上元素的位置、大小等。
- 🎨 重绘:重新绘制外观发生变化的元素。
回流和重绘的频率取决于屏幕刷新率。
理想情况下,每秒最多发生 60、75 次或更多次(即屏幕刷新率)。这是因为,回流和重绘相当耗费资源,如果执行频率超过这个范围,会导致性能卡顿。
然而,除非页面上的脚本专门针对这一点进行了优化,很可能在每个动画帧中会发生多次回流或重绘。
通常情况下,浏览器会将它们推迟到下一次屏幕刷新之前。浏览器还很聪明地缓存布局信息,并通过跟踪自上次回流以来的变化,知道是否实际需要重新计算,即样式/布局是否已被"无效化"(invalidated)。但如果脚本在样式被无效化后读取,则浏览器被迫立即重新计算,以提供正确的测量值。
图片来源
因此,要发生强制同步回流,需要按顺序发生两件事:
- 脚本以某种方式修改样式,使其无效化 ,例如添加 CSS 类或直接修改内联样式。从现在起,我将这称为"修改 "(mutation)。
- 然后请求测量 ,这取决于最新的样式,例如元素的高度。从现在起,我将这称为"测量 "(measurement)。
如何避免它?
避免强制回流的操作顺序是:
- 帧开始:浏览器运行优化的重新计算和重绘。
- JavaScript 可以自由读取或"测量 "任何布局值,例如
offsetHeight
、clientHeight
、getBoundingClientRect
、getComputedStyle
、event.offsetX
等。有关"测量"操作的详细列表,如果这些操作在无效的布局上执行,将强制同步回流。 - ... 更多计算
- 帧结束:
requestAnimationFrame
回调触发。 - 重复。
为了实现这一点,一般原则是:
- 批量处理将所有样式修改推迟到帧结束之前。
- 确保任何测量都在回流之后进行,并在样式被无效化之前完成。
步骤 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,而是编写了自己的实现:
- 批量处理并将修改推迟到帧结束时。
- 批量处理并将测量推迟到下一帧开始后,此时样式几乎可以肯定是有效的,刚刚被重新计算。
现在,即使页面上的其他代码在帧期间修改了布局,我自己的代码也不会强制回流。
步骤 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;
});
});
如何测试它?
浏览器内置的性能监控工具可以帮助你发现强制回流。
- 打开性能标签并开始录制
- 在页面上执行你怀疑会导致回流的操作
- 停止录制并检查时间线。强制回流将显示为顶部事件行下方的小紫色框。当你点击这些框时,它还会告诉你哪一行代码导致了这个问题:
总结
布局抖动是由于 JavaScript 之前修改了布局样式无效化了,浏览器缓存的测量值,现在 JavaScript 想要读取布局属性的最新值,导致浏览器被迫执行样式重新计算。
为了避免它:
- 使用
requestAnimationFrame
将所有修改 DOM 的代码批量处理并推迟到帧结束之前。 - 使用
requestAnimationFrame
和高优先级任务调度器(如 Scheduler 或 MessageChannel)将所有测量批量处理并推迟到下一帧开始后。
资源/参考
原文:www.yuque.com/fengjutian/... 《🌊 布局抖动:是什么?如何消除它》