前言
本文总结回流与重绘相关的知识点
回流与重绘的基本概念
重绘(Repaint): 当元素样式发生改变,但不影响其几何属性的时候,浏览器只需要重新绘制这个元素,这个过程被称为重绘。
回流(Reflow): 当页面布局和几何属性发生变化,导致部分或全部元素的尺寸、位置、结构发生改变时,浏览器需要重新计算元素的几何属性和页面的布局,这个过程被称为回流。
重绘不一定会触发回流,回流一定会触发重绘。
触发条件
回流和重绘的触发条件主要包括对 DOM 结构和样式的修改。以下是触发回流和重绘的常见操作:
回流的触发条件:
- 初始化页面。
- 改变窗口大小。
- 添加、删除、更新 DOM 元素。
- 改变元素的位置、尺寸、内容。
- 修改元素的 font、padding、margin 等样式属性。
对于回流与重绘,我们可以直观地通过 chrome 的开发者工具来查看:
比如下面的 js 操作
document.querySelector(".box").addEventListener("click", function () {
// 改变元素尺寸
this.style.width = "200px";
});
通过 chrome 控制面板可以看到:
完整的 demo 请看👉 在线效果预览, 查看示例代码请点击此处
网上有说法: 获取 offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth.
scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 以及调用 getComputedStyle 等属性和方法都会触发回流。
但是从 chrome 控制面板中看到这些现象并不会触发回流,所以这些说法有待商榷。
重绘的触发条件:
- 修改元素的颜色、背景色。
- 修改元素的 visibility、outline、box-shadow 等不影响布局的属性。
比如下面的 js 操作
document.querySelector(".box").addEventListener("click", function () {
// 修改元素颜色
this.style.backgroundColor = "red";
});
完整的 demo 请看👉 在线效果预览, 查看示例代码请点击此处
仔细与上图对比我们可以发现少了Layout 这一步,说明只触发了重绘,没有触发回流。
优化策略与最佳实践
合并多次修改
合并多次 DOM 和样式修改的优势,减少回流和重绘的次数。
// 不优化的写法,会触发多次回流和重绘
element.style.width = "100px";
element.style.height = "100px";
element.style.backgroundColor = "red";
// 优化的写法,合并为一次回流和重绘
element.style.cssText = "width: 100px; height: 100px; background-color: red;";
使用 class 操作
通过修改元素的类而不是直接操作样式,可以利用浏览器对类的处理进行优化。这样可以减少直接样式修改导致的回流和重绘。
// 不优化的写法,会触发回流和重绘
element.style.width = "100px";
element.style.height = "100px";
element.style.backgroundColor = "red";
// 优化的写法,通过修改类名实现
element.classList.add("custom-style");
/* CSS中定义样式 */
.custom-style {
width: 100px;
height: 100px;
background-color: red;
}
离线操作
执行离线 DOM 操作,即在修改大量 DOM 元素之前,先将它们从文档中移除,进行操作后再放回文档。这样可以最小化对页面渲染的直接影响,降低回流和重绘的次数。
// 不优化的写法,会触发多次回流和重绘
for (let i = 0; i < 100; i++) {
document.body.appendChild(document.createElement("div"));
}
// 优化的写法,离线操作
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement("div");
fragment.appendChild(div);
}
document.body.appendChild(fragment);
浏览器中的 Flush 队列
在浏览器的渲染引擎中,当发生 DOM 或样式变化时,引擎并不会立即执行相应的回流和重绘操作。相反,它将这些操作放入一个队列中,这个队列被称为 Flush 队列。
具体的合并机制可以归结为以下几点:
- 批量执行: 浏览器会等待一定时间,收集多个回流和重绘任务,然后一次性执行它们。这样可以减少任务的切换开销,并优化性能。
- 策略性合并: 浏览器会根据一些策略性的考虑,将一些相关联的回流和重绘任务合并在一起。例如,如果某个元素的多个样式同时发生变化,浏览器可能会将它们合并成一个操作,以减少回流和重绘的次数。
- 异步执行: 部分回流和重绘任务可能会被推迟到下一个事件循环(Event Loop)中执行。这意味着在当前事件循环中发生的多个 DOM 和样式变化可能会在下一个事件循环中被合并执行,从而减少了当前事件循环中的任务执行开销。
以下面一段 js 代码为例
const box = document.querySelector(".box");
box.addEventListener("click", () => {
// 多次修改样式,合并为一次回流和重绘
function performMultipleChanges() {
box.style.width = "200px";
box.style.height = "200px";
box.style.backgroundColor = "red";
}
// 调用多次修改,观察合并效果
performMultipleChanges();
performMultipleChanges();
performMultipleChanges();
});
执行效果如下图所示,可以看到chrome对上述的多个style变更以及多次的performMultipleChanges函数调用进行了合并,最终只触发了一次回流和重绘。
值得注意的是,并不是所有的浏览器都做了这些优化的操作,因此作为开发者,还是要提高自己的知识储备,尽量避免不必要的回流和重绘。
本文首发于个人博客前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正