Skip to content

目录

一、理解回流与重绘

为了优化,我们首先要精确理解这两个概念:

  • 回流: 当元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(即重新进行"布局"阶段),这个过程称为"回流"。回流会影响其周围甚至整个页面的其他节点,是开销非常大的一种操作。回流必然导致重绘。
  • 重绘: 当元素的视觉样式(如color, background-color)发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接为该元素重新绘制(即重新进行"绘制"阶段),这个过程称为"重绘"。相比回流,重绘的开销较小。

优化的核心思想:避免不必要的回流,将多次回流合并为一次。

二、常见触发回流与重绘的操作

了解触发时机是避免它们的第一步。

常见触发回流的操作:

  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()

仅触发重绘的操作:

  • 修改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' // 回流 2

2. 批量修改样式与读写分离

问题:在循环中交替读取元素的布局属性和设置样式,会导致"布局抖动"(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') // 一次操作,浏览器会优化渲染

通过以上方法,我们可以有效减少渲染引擎的工作量,让网页动画和交互变得更加流畅。

基于 VitePress 的本地知识库