JS-拒绝页面卡顿:深入理解浏览器的回流与重绘

44 阅读4分钟

前言

在前端性能优化中,DOM 操作的成本是非常昂贵的。作为开发者,我们经常听到“回流”和“重绘”这两个词,它们究竟是什么?为什么会影响性能?又该如何避免?本文将带你从浏览器渲染机制的底层逻辑出发,彻底搞懂这两个概念。

一、 浏览器渲染的核心流程

在讲回流和重绘之前,我们需要先理解浏览器是如何把 HTML 和 CSS 变成屏幕上的像素的。

  1. 构建 DOM 树:解析 HTML。
  2. 构建 CSSOM 树:解析 CSS。
  3. 生成渲染树 (Render Tree) :DOM 树与 CSSOM 树合并,生成只包含可见节点的渲染树。
  4. 布局 (Layout/Reflow) :计算渲染树中每个节点在屏幕上的确切位置和大小。 (这就是回流)
  5. 绘制 (Paint/Repaint) :将像素填充到图层上(颜色、阴影等)。 (这就是重绘)
  6. 合成 (Composite) :将多个图层合并并在屏幕上展示。

二、 什么是回流 (Reflow)?

概念: 当 Render Tree 中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器需要重新计算元素在设备视口(Viewport)内的确切位置和大小。这个过程称为回流(Reflow),也叫布局(Layout)。

❌ 触发回流的操作: 回流的成本很高,以下操作均会触发:

  1. 页面首次渲染(无法避免)。

  2. 浏览器窗口大小发生改变(Resize)。

  3. 元素尺寸或位置发生改变

    • 盒子模型属性:width, height, padding, margin, border, min-height
    • 定位与浮动:top, left, position, float, clear
  4. 元素内容变化

    • 文字数量变化、图片大小变化。
    • input 框输入文字。
  5. DOM 结构变化:添加或删除可见的 DOM 元素。

  6. CSS 伪类激活:例如 :hover

  7. 文本结构变化

    • text-align, overflow, font-weight, font-family, line-height, font-size, vertical-align

三、 什么是重绘 (Repaint)?

概念: 当页面中元素样式的改变不影响它在文档流中的位置(即几何特征不变),只是影响了元素的外观(如颜色),浏览器会将新样式赋予给元素并重新绘制它。这个过程称为重绘。

🎨 触发重绘的属性: 主要涉及视觉外观的属性:

  • color
  • border-style, border-radius
  • visibility
  • text-decoration
  • background, background-image, background-size
  • outline, 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,而是通过修改 classcsstext 一次性修改。

    // ❌ 糟糕的写法
    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 硬件加速,不会触发回流甚至重绘(走合成线程)。