6.性能优化实战:减少回流与重绘
查看飞书原文 ↗3,029 字符
一、 理解回流 (Reflow) 与重绘 (Repaint)
为了优化,我们首先要精确理解这两个概念:
•
回流 (Reflow): 当元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(即重新进行“布局”阶段),这个过程称为“回流”。回流会影响其周围甚至整个页面的其他节点,是开销非常大的一种操作。回流必然导致重绘。
•
重绘 (Repaint): 当元素的视觉样式(如color, background-color)发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接为该元素重新绘制(即重新进行“绘制”阶段),这个过程称为“重绘”。相比回流,重绘的开销较小。
优化的核心思想:避免不必要的回流,将多次回流合并为一次。
二、 常见触发回流与重绘的操作
了解触发时机是避免它们的第一步。
常见触发回流 (Reflow) 的操作:
1.
页面首次渲染:这是不可避免的回流。
2.
DOM节点操作:增加、删除或修改DOM节点。
3.
元素几何属性变更:修改width, height, padding, margin, border等。
4.
元素位置变更:position, top, left等。
5.
字体变更:修改font-size, font-family等。
6.
窗口尺寸变更:resize事件触发。
7.
获取特定的布局属性:这是最容易被忽略的一点!当你用JavaScript读取以下属性时,浏览器为了返回精确的值,会强制立即执行一次回流:
◦
offsetTop, offsetLeft, offsetWidth, offsetHeight
◦
scrollTop, scrollLeft, scrollWidth, scrollHeight
◦
clientTop, clientLeft, clientWidth, clientHeight
◦
getComputedStyle()
仅触发重绘 (Repaint) 的操作:
•
修改color, background-color, visibility, outline, box-shadow等不影响布局的样式。
三、 优化实战:具体的编码技巧
1. 批量修改DOM
问题:当需要向DOM中添加多个元素时,如果直接在循环中逐个appendChild,每次操作都会触发一次回流。
// 反例:会触发多次回流
const container = document.getElementById('container');
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
el.textContent = i;
container.appendChild(el); // 每次循环都可能触发回流
}
优化技巧:
•
使用 DocumentFragment: DocumentFragment是一个存在于内存中的轻量级DOM容器,对它的所有操作都不会触发回流。我们可以先把所有新节点添加到Fragment中,最后再将Fragment一次性添加到真实DOM中。
// 优例:只触发一次回流
const container = document.getElementById('container');
const fragment = document.createDocumentFragment(); // 创建文档片段
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
el.textContent = i;
fragment.appendChild(el); // 在内存中操作,不触发回流
}
container.appendChild(fragment); // 一次性插入,触发单次回流
•
隐藏元素后操作:将需要多次操作的元素先display: none,完成所有操作后,再恢复显示。这会产生两次回流(隐藏和显示),但远优于N次。
const container = document.getElementById('container');
container.style.display = 'none'; // 回流 1
// ... 在此进行多次DOM操作 ...
container.style.display = 'block'; // 回流 2
2. 批量修改样式与读写分离
问题:在循环中交替读取元素的布局属性和设置样式,会导致“布局抖动”(Layout Thrashing)。浏览器被迫在每次循环中都强制回流。
// 反例:布局抖动
const elements = document.querySelectorAll('.box');
for (let i = 0; i < elements.length; i++) {
// 读取 offsetWidth (读操作),强制浏览器回流
const width = elements[i].offsetWidth;
// 设置 style.width (写操作),再次导致布局变更
elements[i].style.width = width + 10 + 'px';
}
优化技巧:读写分离 (Read/Write Separation)
将读操作和写操作完全分开,先批量读取所有需要的值并缓存,然后批量进行样式更新。
// 优例:读写分离
const elements = document.querySelectorAll('.box');
const widths = [];
// 读操作
for (let i = 0; i < elements.length; i++) {
widths.push(elements[i].offsetWidth);
}
// 写操作
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px';
}
优化技巧:使用 classList
对于复杂的样式变更,推荐将样式组合定义在一个CSS类中,然后通过JavaScript切换class。浏览器可以更高效地处理class的变更。
/* style.css */
.large-box {
width: 200px;
height: 200px;
border: 2px solid blue;
}// JS
const box = document.getElementById('my-box');
// 替代 style.width, style.height 等多次修改
box.classList.add('large-box'); // 一次操作,浏览器会优化渲染
通过以上方法,我们可以有效减少渲染引擎的工作量,让网页动画和交互变得更加流畅。