深入理解浏览器渲染机制:重排(Reflow)与重绘(Repaint)—— 下篇

129 阅读9分钟

摘要

在《深入理解浏览器渲染机制:重排(Reflow)与重绘(Repaint)—— 上篇》中,我们详细探讨了浏览器渲染的基本流程,以及重排和重绘的定义、区别与触发条件。我们了解到,重排是性能开销最大的操作,因为它涉及到布局的重新计算,并且必然伴随着重绘。而重绘虽然开销相对较小,但频繁发生同样会影响用户体验。本篇作为系列文章的下篇,将聚焦于前端性能优化的核心实践:如何识别、避免不必要的重排和重绘,以及如何利用现代浏览器特性(如GPU加速)来优化渲染性能。通过掌握这些优化策略,开发者可以显著提升网页的流畅度和响应速度,为用户带来更优质的体验。

1. 识别与避免不必要的重排和重绘

性能优化的第一步是识别问题。在开发过程中,我们应该警惕那些可能导致频繁重排和重绘的操作。

1.1 避免强制同步布局(Forced Synchronous Layout)

浏览器通常会优化布局和绘制操作,将多个DOM或样式更改操作合并为一次,以减少重排和重绘的次数。这个机制被称为渲染队列异步更新队列。然而,当我们通过JavaScript查询某些需要最新布局信息的属性时,浏览器为了提供准确的值,会强制清空渲染队列,立即执行所有待处理的布局操作,这就会导致一次强制同步布局(Forced Synchronous Layout),从而引发重排。

常见触发强制同步布局的属性和方法

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (IE中是currentStyle)
  • getBoundingClientRect()

示例:反模式

const el = document.getElementById("myElement");
el.style.width = (el.offsetWidth + 10) + "px"; // 第一次读取 offsetWidth 触发重排,第二次修改 width 再次触发重排

在这个例子中,el.offsetWidth会强制浏览器立即计算最新的布局,导致一次重排。紧接着,el.style.width的修改又会触发另一次重排。这种“读写交错”的操作模式是性能杀手。

优化策略

  • 读写分离:将所有读取DOM属性的操作集中在一起,然后将所有修改DOM属性的操作集中在一起,避免读写交错。
const el = document.getElementById("myElement");
const currentWidth = el.offsetWidth; // 先读取
el.style.width = (currentWidth + 10) + "px"; // 后写入
  • 使用缓存:如果某个布局属性的值在短时间内不会改变,可以将其缓存起来,避免重复读取。

1.2 批量修改DOM

频繁地对DOM进行增、删、改操作是导致重排和重绘的常见原因。每次操作都可能触发一次或多次布局更新。

优化策略

  1. 离线DOM操作

    • 使用DocumentFragment:创建一个文档片段,将所有DOM操作在这个片段上完成,然后一次性将片段添加到文档中。DocumentFragment是一个轻量级的文档对象,它的操作不会触发DOM树的重新渲染。
    • 克隆节点并修改:先将要修改的元素克隆一份,在克隆的节点上进行所有修改,然后用修改后的克隆节点替换原有的节点。
    • 设置display: none:在对元素进行大量DOM操作之前,先将其display设置为none,这样元素会脱离文档流,不参与布局。操作完成后再将其display恢复,这样只会触发两次重排(一次隐藏,一次显示)。
  2. CSS Class合并修改

    • 避免直接通过JavaScript多次修改元素的style属性。相反,应该将所有样式修改封装在一个CSS类中,然后通过添加/移除这个类来一次性应用所有样式。

反模式

const el = document.getElementById("myElement");
el.style.padding = "10px";
el.style.border = "1px solid red";
el.style.margin = "5px";
// 每次修改都可能触发重排或重绘

优化

.my-new-style {
  padding: 10px;
  border: 1px solid red;
  margin: 5px;
}
const el = document.getElementById("myElement");
el.classList.add("my-new-style"); // 一次性触发重排/重绘

1.3 避免使用table布局

正如上篇笔记中提到的,<table>布局的特性使其在局部内容改变时,可能导致整个表格的重新布局。这是因为表格的单元格尺寸相互依赖,一个单元格的变化可能连锁反应到整个表格的布局。因此,应尽量避免使用<table>进行非数据展示的布局。

2. 渲染性能优化:利用CSS属性与GPU加速

并非所有的CSS属性都会触发重排。有些属性只会触发重绘,而有些属性甚至可以利用GPU进行硬件加速,完全不触发重排和重绘,只在合成阶段进行。

2.1 区分触发类型:重排、重绘、合成

我们可以将CSS属性对渲染的影响分为三类:

  1. 触发重排(Reflow) :改变元素几何属性的属性,如width, height, padding, margin, border, top, left, font-size, display等。
  2. 只触发重绘(Repaint) :改变元素外观但不影响布局的属性,如color, background-color, visibility, outline, text-decoration, box-shadow等。
  3. 触发合成(Compositing Only) :这些属性通常由GPU加速,不触发重排和重绘,只在合成层上进行操作。主要包括transformopacity

2.2 CSS GPU硬件加速的原理与应用

GPU硬件加速(Graphics Processing Unit Hardware Acceleration),又称CSS3硬件加速,是利用GPU进行渲染,减少CPU操作的一种优化方案。当某些元素被提升到独立的合成层(Composited Layer)时,对这些元素的某些属性(如transformopacity)的改变,将不再触发重排和重绘,而是在GPU上直接进行合成操作,从而大大提高动画的流畅度。

原理

  1. 分层(Layering) :浏览器在渲染过程中,会将页面内容划分为多个独立的图层。例如,拥有z-indexposition: fixedtransformopacityfilter等属性的元素,或者videocanvas等元素,可能会被提升到独立的合成层。
  2. 光栅化(Rasterization) :每个图层会被独立地光栅化(将矢量图形转换为像素)。
  3. 合成(Compositing) :光栅化后的图层会被发送到GPU,GPU负责将这些图层合成为最终的图像,并显示在屏幕上。这个过程非常高效。

为什么transformopacity可以GPU加速?

  • transform(如translate, scale, rotate)和opacity(透明度)属性的改变,不影响元素的几何布局,也不需要重新计算元素的尺寸和位置。它们只涉及到像素的移动、缩放或透明度变化。
  • 当这些属性作用于一个独立的合成层时,GPU可以直接对该图层的位图进行操作,而无需CPU介入布局和绘制阶段。这使得动画非常流畅,即使在复杂的页面中也能保持60fps的帧率。

如何开启GPU硬件加速?

  • 使用transformopacity进行动画:这是最推荐的方式。例如,使用transform: translate(X, Y)代替top/left来移动元素。
  • 使用will-change属性will-change属性可以提前告知浏览器哪些属性将要发生变化,从而让浏览器提前进行优化(如创建独立的合成层)。但应谨慎使用,避免过度优化导致内存消耗增加。
  • 一些老旧的hack方式:例如transform: translateZ(0)backface-visibility: hidden,这些属性本身不会产生视觉效果,但可以强制浏览器为元素创建独立的合成层。但在现代浏览器中,通常不再需要这些hack。

示例:使用transform优化动画

反模式

.box {
  position: absolute;
  left: 0;
  transition: left 0.3s ease-out;
}
.box.move {
  left: 100px; /* 触发重排和重绘 */
}

优化

.box {
  transition: transform 0.3s ease-out;
}
.box.move {
  transform: translateX(100px); /* 只触发合成 */
}

3. 优化策略总结与实践建议

综合上篇和下篇的内容,以下是减少重排和重绘,提升页面渲染性能的关键策略:

  1. 避免频繁的DOM操作

    • 使用DocumentFragmentdisplay: none进行离线DOM操作。
    • 将多次样式修改合并为一次,通过修改CSS类名来实现。
  2. 避免强制同步布局

    • 将DOM读取操作和DOM写入操作分离,避免读写交错。
    • 缓存布局属性的值,避免重复读取。
  3. 使用CSS3动画代替JavaScript动画

    • 优先使用transitionanimation等CSS动画,它们通常由浏览器进行优化,性能更好。
  4. 利用GPU硬件加速

    • 对动画元素使用transformopacity属性,而不是top/leftwidth/height
    • 合理使用will-change属性。
  5. 减少不必要的DOM深度和复杂性

    • 扁平化的DOM结构有助于浏览器更快地计算布局。
  6. 优化图片和媒体资源

    • 使用适当的图片格式和压缩,避免大尺寸图片导致额外的布局计算。
  7. 虚拟列表/无限滚动

    • 对于长列表,只渲染可视区域内的内容,减少DOM元素的数量。
  8. 使用性能分析工具

    • 利用Chrome DevTools的Performance面板,可以直观地看到页面渲染过程中的重排和重绘情况,帮助定位性能瓶颈。

4. 总结:性能优化是一场持久战

重排和重绘是前端性能优化中绕不开的话题。它们是浏览器为了呈现页面而必须执行的操作,但频繁或不必要的触发会严重影响用户体验。通过深入理解浏览器渲染的底层机制,掌握重排和重绘的触发条件,并积极运用各种优化策略,我们可以有效地减少它们的发生频率和开销。

性能优化并非一蹴而就,而是一场需要持续关注和实践的持久战。在日常开发中,培养良好的编码习惯,时刻关注代码对渲染性能的影响,并善用浏览器提供的性能分析工具,将使我们能够构建出更流畅、更响应迅速的Web应用。

希望通过本系列文章,读者能够对重排和重绘有一个全面而深入的理解,并将其应用于实际项目中,成为一名更优秀的前端性能优化工程师。