前言
在前端性能优化中,DOM 操作的成本是非常昂贵的。作为开发者,我们经常听到“回流”和“重绘”这两个词,它们究竟是什么?为什么会影响性能?又该如何避免?本文将带你从浏览器渲染机制的底层逻辑出发,彻底搞懂这两个概念。
一、 浏览器渲染的核心流程
在讲回流和重绘之前,我们需要先理解浏览器是如何把 HTML 和 CSS 变成屏幕上的像素的。
- 构建 DOM 树:解析 HTML。
- 构建 CSSOM 树:解析 CSS。
- 生成渲染树 (Render Tree) :DOM 树与 CSSOM 树合并,生成只包含可见节点的渲染树。
- 布局 (Layout/Reflow) :计算渲染树中每个节点在屏幕上的确切位置和大小。 (这就是回流)
- 绘制 (Paint/Repaint) :将像素填充到图层上(颜色、阴影等)。 (这就是重绘)
- 合成 (Composite) :将多个图层合并并在屏幕上展示。
二、 什么是回流 (Reflow)?
概念: 当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器需要重新计算元素在设备视口(Viewport)内的确切位置和大小。这个过程称为回流(Reflow),也叫布局(Layout)。
❌ 触发回流的操作: 回流的成本很高,以下操作均会触发:
-
页面首次渲染(无法避免)。
-
浏览器窗口大小发生改变(Resize)。
-
元素尺寸或位置发生改变:
- 盒子模型属性:
width,height,padding,margin,border,min-height - 定位与浮动:
top,left,position,float,clear
- 盒子模型属性:
-
元素内容变化:
- 文字数量变化、图片大小变化。
- input 框输入文字。
-
DOM 结构变化:添加或删除可见的 DOM 元素。
-
CSS 伪类激活:例如
:hover。 -
文本结构变化:
text-align,overflow,font-weight,font-family,line-height,font-size,vertical-align
三、 什么是重绘 (Repaint)?
概念: 当页面中元素样式的改变不影响它在文档流中的位置(即几何特征不变),只是影响了元素的外观(如颜色),浏览器会将新样式赋予给元素并重新绘制它。这个过程称为重绘。
🎨 触发重绘的属性: 主要涉及视觉外观的属性:
colorborder-style,border-radiusvisibilitytext-decorationbackground,background-image,background-sizeoutline,box-shadow
四、 核心区别与性能影响
1. 相互关系
记住这个定律: 回流必将引起重绘,但重绘不一定引起回流。
2. 性能代价
回流的代价远高于重绘。回流通常涉及到 DOM 树的重新计算,有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也可能产生回流(牵一发而动全身)。
3. 浏览器的“惰性”优化与强制同步布局
现代浏览器为了优化性能,会维护一个渲染队列。它会将所有引起回流和重绘的操作放入队列中,等待队列任务达到阈值或一定时间后,进行批处理(Batch Execution),将多次变动合并为一次。
⚠️ 强制清空队列(强制回流): 如果你在修改样式的过程中,访问了以下属性,浏览器为了返回最精确的当前值,会被迫立刻清空队列,触发同步回流。这会打破浏览器的优化机制,应尽量避免在循环中调用:
- Offset 类:
offsetWidth,offsetHeight,offsetTop,offsetLeft - Scroll 类:
scrollWidth,scrollHeight,scrollTop,scrollLeft - Client 类:
clientWidth,clientHeight,clientTop,clientLeft - 方法类:
getComputedStyle(),getBoundingClientRect(),scrollIntoView()
五、 实战:如何避免回流与重绘?
核心思路: 减少回流/重绘的次数,缩小回流/重绘的影响范围。
1. CSS 优化
-
避免使用
table布局:table中任何一个小改动都可能导致整个table的重新布局。 -
集中修改样式:不要一条条修改 DOM 的 style,而是通过修改
class或csstext一次性修改。// ❌ 糟糕的写法 el.style.left = '10px'; el.style.top = '20px'; // ✅ 推荐写法 el.className += ' new-class'; -
DOM 树末端改变:尽量在 DOM 树的最末端改变 class,限制回流范围。
-
动画优化:对具有复杂动画的元素使用绝对定位(
position: absolute / fixed),使它脱离文档流,这样它的变化不会影响到其他元素,只会触发自身的重绘或局部回流。
2. JavaScript 优化
-
避免频繁操作 DOM:
- 使用
documentFragment创建一个文档碎片,在碎片上进行多次 DOM 操作,最后一次性添加到文档中。 - 先将元素设置
display: none(触发一次回流),进行多次修改后,再恢复显示(再触发一次回流)。
- 使用
-
缓存布局信息:
// ❌ 糟糕的写法(在循环中读取 offset,导致强制回流) for(let i=0; i<len; i++) { el.style.left = el.offsetLeft + 1 + 'px'; } // ✅ 推荐写法(读写分离,缓存值) let left = el.offsetLeft; for(let i=0; i<len; i++) { left++; el.style.left = left + 'px'; } -
使用 CSS3 硬件加速:使用
transform,opacity,filters等属性会触发 GPU 硬件加速,不会触发回流甚至重绘(走合成线程)。