6.性能优化实战:减少回流与重绘
一、 理解回流 (Reflow) 与重绘 (Repaint)
为了优化,我们首先要精确理解这两个概念:
回流 (Reflow): 当元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(即重新进行“布局”阶段),这个过程称为“回流”。回流会影响其周围甚至整个页面的其他节点,是开销非常大的一种操作。回流必然导致重绘。
重绘 (Repaint): 当元素的视觉样式(如
color,background-color)发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接为该元素重新绘制(即重新进行“绘制”阶段),这个过程称为“重绘”。相比回流,重绘的开销较小。
优化的核心思想:避免不必要的回流,将多次回流合并为一次。
二、 常见触发回流与重绘的操作
了解触发时机是避免它们的第一步。
常见触发回流 (Reflow) 的操作:
页面首次渲染:这是不可避免的回流。
DOM节点操作:增加、删除或修改DOM节点。
元素几何属性变更:修改
width,height,padding,margin,border等。元素位置变更:
position,top,left等。字体变更:修改
font-size,font-family等。窗口尺寸变更:
resize事件触发。获取特定的布局属性:这是最容易被忽略的一点!当你用JavaScript读取以下属性时,浏览器为了返回精确的值,会强制立即执行一次回流:
offsetTop,offsetLeft,offsetWidth,offsetHeightscrollTop,scrollLeft,scrollWidth,scrollHeightclientTop,clientLeft,clientWidth,clientHeightgetComputedStyle()
仅触发重绘 (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中。JavaScript// 优例:只触发一次回流 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次。JavaScriptconst 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'); // 一次操作,浏览器会优化渲染通过以上方法,我们可以有效减少渲染引擎的工作量,让网页动画和交互变得更加流畅。