回流重绘原理及优化

10 阅读4分钟

1. 核心概念与触发机制

在浏览器的渲染流水线中,当 DOM 的更新或 CSS 样式的修改打破了页面的现有状态时,浏览器会重新计算并更新屏幕显示。这个过程主要分为两个阶段:

  • 回流(Reflow / Layout) :又称“重排”。当元素的几何属性发生变化时(如宽高、内外边距、位置、字体大小),或者 DOM 结构发生改变(增删节点)、浏览器窗口大小改变时,浏览器必须重新计算该元素及其受影响节点的几何信息。回流往往具有级联效应,一个父元素的尺寸变化可能迫使它的子元素、兄弟元素甚至整个页面重新计算布局,因此代价极其昂贵。
  • 重绘(Repaint / Paint) :当元素的外观属性发生变化(如颜色、背景色、阴影等),但不影响其几何位置和尺寸时,浏览器只需重新绘制该元素的像素。由于跳过了复杂的布局计算,重绘的成本远低于回流。

两者的关系回流必定引发重绘,但重绘不一定引发回流。因为只要布局发生了改变,元素的视觉表现必然也要随之更新;但如果只是换个颜色,完全不需要重新计算位置。

2. 常见的触发场景

  • 触发回流的典型操作

    • 修改 width, height, margin, padding, border 等几何属性。
    • 增加或删除 DOM 节点。
    • 调整浏览器窗口大小(Resize)。
    • 强制同步布局:在 JS 中读取某些需要实时计算的属性(如 offsetWidth, scrollTop, getComputedStyle() 等),这会逼迫浏览器立即执行回流以返回最新值。
  • 仅触发重绘的典型操作

    • 修改 color, background-color, box-shadow, visibility 等样式。

3. 终极优化策略

优化的核心目标是:尽量减少回流和重绘的频率与成本,最好能直接跳过它们,进入合成(Composite)阶段

3.1 巧妙利用 CSS 硬件加速(GPU 加速)

现代浏览器引入了“合成层”的概念。如果我们能让动画或变化只发生在最后的合成阶段,就可以完全避开主线程的 Layout 和 Paint,交由 GPU 处理。

  • 最佳实践:在做位移动画时,使用 transform: translateX() 代替传统的 top/left/margin-left;做透明度动画时使用 opacity。这两个属性的修改不会引起回流和重绘。
  • 提前创建图层:对于频繁变动的复杂节点,可以使用 will-change: transformtransform: translateZ(0) 将其提升为独立图层,防止它的变化牵连其他节点。但注意不要滥用,以免占用过多内存。

3.2 JavaScript 层面的优化

  • 批量修改样式:避免逐个修改节点的样式。推荐一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
  • 离线操作 DOM:如果需要多次修改某个 DOM 元素,可以先将其设置为 display: none(脱离文档流后修改不会触发回流),操作完成后再将其显示出来。也可以使用 documentFragment 在内存中构建好节点后,再一次性插入到文档中。
  • 缓存 DOM 映射值:如果在循环中需要读取元素的几何属性,务必在循环外部用变量保存这些值,避免在循环内部反复触发强制同步布局。
  • 使用 requestAnimationFrame:在处理高频动画时,使用 requestAnimationFrame 替代 setInterval/setTimeout,它可以保证回调函数在浏览器的下一次重绘之前执行,使动画更加顺滑且不易掉帧。

3.3 CSS 编写规范优化

  • 简化选择器层级:浏览器的 CSS 解析器是从右向左匹配规则的。过深的嵌套和过于复杂的选择器会增加匹配耗时,建议保持 CSS 规则层级在 3 层左右。
  • 避免使用 Table 布局:Table 标签在渲染时通常需要等待所有单元格内容加载完毕才能确定整体布局,极易引发大面积的回流,推荐使用 Flexbox 或 Grid 替代。
  • 延迟非关键 CSS:通过拆分 CSS 文件、内联关键 CSS 或使用媒体查询异步加载非首屏样式,减少初始渲染时的阻塞时间。