Appearance
目录
一、理解回流与重绘
为了优化,我们首先要精确理解这两个概念:
- 回流: 当元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(即重新进行"布局"阶段),这个过程称为"回流"。回流会影响其周围甚至整个页面的其他节点,是开销非常大的一种操作。回流必然导致重绘。
- 重绘: 当元素的视觉样式(如color, background-color)发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接为该元素重新绘制(即重新进行"绘制"阶段),这个过程称为"重绘"。相比回流,重绘的开销较小。
优化的核心思想:避免不必要的回流,将多次回流合并为一次。
二、常见触发回流与重绘的操作
了解触发时机是避免它们的第一步。
常见触发回流的操作:
- 页面首次渲染:这是不可避免的回流。
- DOM节点操作:增加、删除或修改DOM节点。
- 元素几何属性变更:修改width, height, padding, margin, border等。
- 元素位置变更:position, top, left等。
- 字体变更:修改font-size, font-family等。
- 窗口尺寸变更:resize事件触发。
- 获取特定的布局属性:这是最容易被忽略的一点!当你用JavaScript读取以下属性时,浏览器为了返回精确的值,会强制立即执行一次回流:
- offsetTop, offsetLeft, offsetWidth, offsetHeight
- scrollTop, scrollLeft, scrollWidth, scrollHeight
- clientTop, clientLeft, clientWidth, clientHeight
- getComputedStyle()
仅触发重绘的操作:
- 修改color, background-color, visibility, outline, box-shadow等不影响布局的样式。
三、优化实战:具体的编码技巧
1. 批量修改DOM
问题:当需要向DOM中添加多个元素时,如果直接在循环中逐个appendChild,每次操作都会触发一次回流。
javascript
// 反例:会触发多次回流
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次。
javascript
const container = document.getElementById('container')
container.style.display = 'none' // 回流 1
// ... 在此进行多次DOM操作 ...
container.style.display = 'block' // 回流 22. 批量修改样式与读写分离
问题:在循环中交替读取元素的布局属性和设置样式,会导致"布局抖动"(Layout Thrashing)。浏览器被迫在每次循环中都强制回流。
javascript
// 反例:布局抖动
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'
}优化技巧:读写分离
将读操作和写操作完全分开,先批量读取所有需要的值并缓存,然后批量进行样式更新。
javascript
// 优例:读写分离
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的变更。
css
/* style.css */
.large-box {
width: 200px;
height: 200px;
border: 2px solid blue;
}javascript
// JS
const box = document.getElementById('my-box')
// 替代 style.width, style.height 等多次修改
box.classList.add('large-box') // 一次操作,浏览器会优化渲染通过以上方法,我们可以有效减少渲染引擎的工作量,让网页动画和交互变得更加流畅。