MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

CSS 减少重绘与回流提升页面性能的优化方案

2022-01-067.0k 阅读

理解回流与重绘

在深入探讨如何通过 CSS 减少重绘与回流来提升页面性能之前,我们首先需要对回流(reflow)和重绘(repaint)的概念有清晰的认识。

回流(Reflow)

回流,也被称为重排,是浏览器渲染机制中的一个关键环节。当网页的布局发生变化时,浏览器需要重新计算元素的几何属性(例如位置、尺寸等),并将其重新绘制到页面上。这种重新计算和重新布局的过程就叫做回流。

触发回流的场景非常多,常见的有以下几种:

  1. 元素的几何属性变化:当改变元素的 widthheightmarginpaddingborder 等属性时,会触发回流。例如:
/* HTML 结构 */
<div id="box">这是一个盒子</div>

/* CSS 样式 */
#box {
  width: 100px;
  height: 100px;
  background-color: lightblue;
}

/* JavaScript 改变样式触发回流 */
var box = document.getElementById('box');
box.style.width = '200px';

在上述代码中,通过 JavaScript 改变了 box 元素的宽度,这就会导致浏览器重新计算该元素及其相关元素的布局,从而触发回流。 2. 添加或删除可见的 DOM 元素:当向 DOM 树中添加或移除一个元素时,浏览器需要重新计算整个文档的布局,进而触发回流。例如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div id="parent">
    <div id="child1">子元素 1</div>
  </div>
  <script>
    var parent = document.getElementById('parent');
    var newChild = document.createElement('div');
    newChild.id = 'child2';
    newChild.textContent = '子元素 2';
    parent.appendChild(newChild);
  </script>
</body>

</html>

在这段代码中,通过 JavaScript 向 parent 元素中添加了一个新的子元素 child2,这会使浏览器重新计算 parent 及其相关元素的布局,触发回流。 3. 改变字体大小:因为字体大小的改变会影响文本的尺寸,进而影响元素的布局,所以也会触发回流。例如:

body {
  font-size: 16px;
}

/* JavaScript 改变字体大小触发回流 */
document.body.style.fontSize = '18px';

这里通过 JavaScript 改变了 body 元素的字体大小,会导致浏览器重新计算页面中所有与文本布局相关的元素,引发回流。

重绘(Repaint)

重绘是指当元素的外观发生变化,但布局没有改变时,浏览器重新绘制该元素的过程。例如,改变元素的 colorbackground - colorvisibility 等属性,不会影响元素的布局,只会触发重绘。

以下是一些触发重绘的常见场景:

  1. 改变元素的颜色:当改变元素的文本颜色或背景颜色时,会触发重绘。例如:
/* HTML 结构 */
<p id="text">这是一段文本</p>

/* CSS 样式 */
#text {
  color: black;
}

/* JavaScript 改变颜色触发重绘 */
var text = document.getElementById('text');
text.style.color ='red';

在上述代码中,通过 JavaScript 将 text 元素的文本颜色从黑色改为红色,这一操作仅改变了元素的外观,不会影响其布局,因此只会触发重绘。 2. 改变元素的背景图像:更改元素的 background - image 属性也会触发重绘。例如:

/* HTML 结构 */
<div id="bgBox">这是一个有背景的盒子</div>

/* CSS 样式 */
#bgBox {
  width: 200px;
  height: 200px;
  background-image: url('image1.jpg');
}

/* JavaScript 改变背景图像触发重绘 */
var bgBox = document.getElementById('bgBox');
bgBox.style.backgroundImage = 'url('image2.jpg')';

这里通过 JavaScript 改变了 bgBox 元素的背景图像,由于没有改变元素的布局,所以只会引发重绘。

需要注意的是,回流通常会伴随着重绘,因为布局的改变往往会导致元素外观的重新绘制。而重绘不一定会引发回流,当仅改变元素的外观属性且不影响布局时,只会发生重绘。

回流与重绘对性能的影响

回流和重绘操作在浏览器渲染页面过程中是非常消耗性能的。这是因为它们涉及到浏览器对页面布局和绘制的重新计算与处理。

回流的性能消耗

  1. 计算成本高:回流需要浏览器重新计算页面中所有受影响元素的几何属性。当页面结构复杂,元素众多时,这个计算过程会变得非常耗时。例如,一个包含大量嵌套 div 元素且具有复杂 CSS 布局的页面,当其中一个元素的宽度发生变化时,浏览器需要从该元素开始,向上递归到根元素,重新计算所有相关元素的位置和尺寸,这个过程可能涉及到大量的数学运算,对 CPU 资源的消耗较大。
  2. 影响范围广:回流不仅仅影响发生变化的元素本身,还会对其祖先元素以及依赖于该元素布局的其他元素产生影响。例如,在一个使用 Flexbox 布局的容器中,如果其中一个子元素的高度发生改变,可能会导致整个 Flex 容器的布局重新调整,进而影响到其他子元素的位置和尺寸,使得更多的元素需要重新计算布局,进一步增加了性能开销。

重绘的性能消耗

虽然重绘相比回流,不需要重新计算布局,但它仍然需要浏览器重新绘制受影响的元素。在现代浏览器中,绘制操作通常会使用 GPU 加速来提高效率,但当重绘的元素数量较多或者重绘频繁发生时,仍然会对性能产生明显的影响。例如,在一个动画效果中,如果频繁地改变元素的颜色,导致大量的重绘操作,会使页面的帧率下降,出现卡顿现象。

综上所述,回流和重绘操作如果频繁发生,会严重影响页面的性能,导致页面加载缓慢、响应迟钝以及动画卡顿等问题。因此,在前端开发中,我们需要尽可能地减少回流和重绘的发生,以提升页面的性能。

CSS 减少回流与重绘的优化方案

批量操作 DOM

在对 DOM 元素进行操作时,如果频繁地单个操作,会导致多次回流或重绘。通过批量操作 DOM,可以将多个操作合并为一次,从而减少回流和重绘的次数。

  1. 使用 DocumentFragmentDocumentFragment 是一个轻量级的 DOM 容器,它存在于内存中,不会影响页面的渲染。我们可以先将需要操作的元素添加到 DocumentFragment 中,进行批量操作后,再将 DocumentFragment 添加到页面的 DOM 树中。这样只会触发一次回流和重绘。例如:
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <ul id="list"></ul>
  <script>
    var list = document.getElementById('list');
    var fragment = document.createDocumentFragment();
    var items = ['苹果', '香蕉', '橙子'];
    items.forEach(function (item) {
      var li = document.createElement('li');
      li.textContent = item;
      fragment.appendChild(li);
    });
    list.appendChild(fragment);
  </script>
</body>

</html>

在上述代码中,我们首先创建了一个 DocumentFragment,然后将多个 li 元素添加到 fragment 中,最后将 fragment 添加到 list 元素中。这样,整个过程只触发了一次回流和重绘,而如果逐个将 li 元素添加到 list 中,会触发多次回流和重绘。 2. 改变类名:通过一次性改变元素的类名,而不是逐个修改元素的样式属性,也可以实现批量操作。因为修改类名会一次性应用该类中定义的所有样式,只触发一次回流和重绘。例如:

/* 定义两个类 */
.normal {
  width: 100px;
  height: 100px;
  background-color: lightblue;
}

.modified {
  width: 200px;
  height: 200px;
  background-color: pink;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="box" class="normal"></div>
  <script>
    var box = document.getElementById('box');
    // 一次性改变类名
    box.classList.remove('normal');
    box.classList.add('modified');
  </script>
</body>

</html>

在这段代码中,通过一次性改变 box 元素的类名,从 normal 类切换到 modified 类,所有相关的样式变化会一次性应用,只触发一次回流和重绘。如果逐个修改 box 元素的 widthheightbackground - color 属性,会触发多次回流和重绘。

避免频繁读取元素的几何属性

当我们读取元素的几何属性(如 offsetTopoffsetLeftclientWidthclientHeight 等)时,浏览器需要即时计算这些属性的值,这可能会导致回流。如果在一个循环中频繁读取这些属性,就会引发多次回流,严重影响性能。

  1. 缓存几何属性值:在需要多次使用元素的几何属性值时,可以先将其缓存起来,避免重复读取。例如:
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div id="box">这是一个盒子</div>
  <script>
    var box = document.getElementById('box');
    // 缓存几何属性值
    var boxWidth = box.clientWidth;
    var boxHeight = box.clientHeight;
    for (var i = 0; i < 10; i++) {
      // 使用缓存的值,避免多次读取引发回流
      console.log('盒子宽度: ', boxWidth, '盒子高度: ', boxHeight);
    }
  </script>
</body>

</html>

在上述代码中,我们先将 box 元素的 clientWidthclientHeight 属性值缓存到变量 boxWidthboxHeight 中,然后在循环中使用这些缓存的值,避免了在循环中多次读取 clientWidthclientHeight 属性而引发多次回流。 2. 将读取操作与写入操作分离:尽量将读取元素几何属性的操作和修改元素样式的操作分开执行。例如:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div id="box">这是一个盒子</div>
  <script>
    var box = document.getElementById('box');
    // 先读取几何属性
    var boxTop = box.offsetTop;
    // 然后进行写入操作
    box.style.top = (boxTop + 10) + 'px';
  </script>
</body>

</html>

在这段代码中,我们先读取了 box 元素的 offsetTop 属性,然后再修改其 top 样式属性。这样可以避免在读取属性后立即修改样式,从而减少回流的发生。如果先修改样式再读取属性,浏览器可能需要重新计算布局以获取准确的属性值,导致回流。

使用 CSS3 硬件加速

CSS3 提供了一些属性,可以利用浏览器的 GPU 进行硬件加速,从而提升动画和过渡效果的性能,减少重绘和回流。

  1. 使用 transformopacitytransformopacity 属性的变化不会触发回流,只会触发重绘,并且现代浏览器通常会将这两个属性的动画和过渡效果交给 GPU 处理,实现硬件加速。例如,创建一个元素的淡入动画:
/* HTML 结构 */
<div id="fadeInBox">淡入盒子</div>

/* CSS 样式 */
#fadeInBox {
  width: 100px;
  height: 100px;
  background-color: lightgreen;
  opacity: 0;
  transition: opacity 1s ease - in - out;
}

#fadeInBox.fade - in {
  opacity: 1;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="fadeInBox"></div>
  <script>
    var fadeInBox = document.getElementById('fadeInBox');
    setTimeout(function () {
      fadeInBox.classList.add('fade - in');
    }, 1000);
  </script>
</body>

</html>

在上述代码中,通过改变 opacity 属性来实现淡入效果,这个过程不会触发回流,并且浏览器会利用 GPU 加速动画,提升性能。同样,使用 transform 属性进行元素的平移、旋转、缩放等操作时,也能达到类似的效果。例如:

/* HTML 结构 */
<div id="transformBox">变换盒子</div>

/* CSS 样式 */
#transformBox {
  width: 100px;
  height: 100px;
  background-color: orange;
  transform: translateX(0px);
  transition: transform 1s ease - in - out;
}

#transformBox.move {
  transform: translateX(200px);
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="transformBox"></div>
  <script>
    var transformBox = document.getElementById('transformBox');
    setTimeout(function () {
      transformBox.classList.add('move');
    }, 1000);
  </script>
</body>

</html>

这里通过改变 transform 属性的 translateX 值来实现元素的平移动画,利用 GPU 加速,减少了性能消耗。 2. 使用 will - change 提示will - change 属性可以提前告知浏览器,某个元素在未来可能会发生变化,让浏览器提前做好优化准备。例如,如果你知道一个元素即将进行 transform 动画,可以提前设置 will - change: transform。虽然这个属性本身不会直接触发硬件加速,但它能让浏览器在合适的时机进行优化,减少动画开始时的性能抖动。例如:

/* HTML 结构 */
<div id="willChangeBox">即将变化的盒子</div>

/* CSS 样式 */
#willChangeBox {
  width: 100px;
  height: 100px;
  background-color: purple;
  will - change: transform;
}

#willChangeBox.animate {
  transform: rotate(360deg);
  transition: transform 2s ease - in - out;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="willChangeBox"></div>
  <script>
    var willChangeBox = document.getElementById('willChangeBox');
    setTimeout(function () {
      willChangeBox.classList.add('animate');
    }, 1000);
  </script>
</body>

</html>

在上述代码中,提前设置 will - change: transform 可以让浏览器提前为即将到来的 transform 动画做好准备,可能会在动画执行时减少回流和重绘的性能开销。

优化 CSS 选择器

CSS 选择器的性能也会间接影响回流和重绘的频率。复杂的选择器可能导致浏览器在匹配元素时花费更多的时间,从而影响页面的渲染性能。

  1. 避免使用通配符选择器:通配符选择器(*)会匹配页面中的所有元素,这会极大地增加浏览器的匹配成本。例如:
/* 避免这样的通配符选择器 */
* {
  margin: 0;
  padding: 0;
}

如果需要重置页面的边距和内边距,可以选择更具体的选择器,如:

body,
ul,
ol,
h1,
h2,
h3,
h4,
h5,
h6,
p {
  margin: 0;
  padding: 0;
}

这样只针对特定的元素进行样式设置,减少了浏览器的匹配范围,提高了性能。 2. 减少选择器的深度:选择器的深度指的是选择器中从右到左的元素层级数。选择器深度越深,浏览器匹配元素所需的时间就越长。例如,尽量避免这样的深度过深的选择器:

body div ul li a {
  color: blue;
}

可以通过更直接的方式来选择元素,如给 a 元素添加一个类名,然后使用类选择器:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <style>
    .link {
      color: blue;
    }
  </style>
</head>

<body>
  <div>
    <ul>
      <li><a href="#" class="link">链接</a></li>
    </ul>
  </div>
</body>

</html>

这样通过类选择器直接选择 a 元素,减少了选择器的深度,提高了浏览器匹配元素的效率,间接减少了回流和重绘时的性能开销。

合理使用 CSS Sprites

CSS Sprites 是一种将多个小图标合并成一张大图的技术,通过背景定位(background - position)来显示不同的图标。这种方法可以减少 HTTP 请求数量,同时也有助于减少重绘。

  1. 减少 HTTP 请求:在网页中,如果有多个小图标,每个图标都单独请求一个图片文件,会增加 HTTP 请求的次数。而 HTTP 请求会带来一定的开销,包括建立连接、传输数据等过程。通过将多个小图标合并成一张大图,只需要一次 HTTP 请求,大大减少了请求开销,加快了页面的加载速度。例如,假设有三个小图标 icon1.pngicon2.pngicon3.png,可以将它们合并成一张 sprite.png
  2. 减少重绘:当页面中有多个小图标,并且这些图标会随着用户操作或页面状态变化而改变时,如果每个图标是单独的图片,每次图标变化都可能触发重绘。而使用 CSS Sprites,通过改变 background - position 属性来切换图标,不会触发重绘,因为元素的外观变化是通过背景定位实现的,而不是加载新的图片。例如:
/* 定义 CSS Sprites 样式 */
.icon {
  width: 32px;
  height: 32px;
  background - image: url('sprite.png');
  display: inline - block;
}

.icon - 1 {
  background - position: 0 0;
}

.icon - 2 {
  background - position: -32px 0;
}

.icon - 3 {
  background - position: -64px 0;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div class="icon icon - 1"></div>
  <div class="icon icon - 2"></div>
  <div class="icon icon - 3"></div>
</body>

</html>

在上述代码中,通过切换不同的类来改变 background - position,从而显示不同的图标,整个过程不会触发重绘,提升了页面性能。

避免使用 table 布局

table 元素在 HTML 中主要用于展示表格数据,但在过去,有些人会使用 table 来进行页面布局。然而,table 布局会导致回流的范围扩大,影响性能。

  1. 回流范围大table 布局的特点是,当其中一个单元格的内容或样式发生变化时,不仅会影响该单元格本身,还可能会影响整个表格的布局,甚至会影响到其他相关表格(如果页面中有多个关联的表格)。例如,改变一个 td 元素的宽度,可能会导致整个 table 重新计算布局,进而影响到其他行和列的尺寸,引发较大范围的回流。相比之下,使用现代的 CSS 布局方式,如 Flexbox 或 Grid,元素之间的布局影响相对更局部化。例如,在 Flexbox 布局中,一个子元素的尺寸变化通常只会影响到其兄弟元素在 Flex 容器内的布局,不会像 table 布局那样产生广泛的影响。
  2. 渲染性能低table 布局的渲染过程相对复杂,浏览器需要先解析整个 table 的结构,然后才能确定每个单元格的位置和尺寸。这与现代 CSS 布局方式(如 Flexbox 和 Grid)相比,渲染效率较低。现代布局方式允许浏览器更灵活、更高效地计算元素的布局,减少回流和重绘的次数。因此,在进行页面布局时,应尽量避免使用 table 布局,而是选择 Flexbox 或 Grid 等更适合布局的 CSS 技术。

优化动画与过渡效果

动画和过渡效果在提升用户体验的同时,如果处理不当,也会带来性能问题,尤其是频繁的回流和重绘。

  1. 使用 requestAnimationFrame:在 JavaScript 中实现动画时,使用 requestAnimationFrame 代替 setIntervalsetTimeout 可以更好地控制动画的帧率,减少不必要的回流和重绘。requestAnimationFrame 会在浏览器下一次重绘之前调用回调函数,这样可以确保动画的每一帧都在合适的时机进行更新,避免了在浏览器还没有准备好重绘时就进行大量的计算和样式更新,从而减少回流和重绘的次数。例如,实现一个简单的元素移动动画:
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <style>
    #movingBox {
      width: 50px;
      height: 50px;
      background-color: yellow;
      position: relative;
    }
  </style>
</head>

<body>
  <div id="movingBox"></div>
  <script>
    var movingBox = document.getElementById('movingBox');
    var x = 0;
    function moveBox() {
      x += 1;
      movingBox.style.left = x + 'px';
      if (x < 200) {
        requestAnimationFrame(moveBox);
      }
    }
    requestAnimationFrame(moveBox);
  </script>
</body>

</html>

在上述代码中,通过 requestAnimationFrame 来控制 movingBox 元素的移动动画,确保动画的每一帧都在浏览器准备好重绘时进行更新,减少了性能开销。 2. 优化动画的复杂度:尽量避免过于复杂的动画效果,如大量元素同时进行复杂的旋转、缩放、平移等操作。复杂的动画会导致大量的回流和重绘,消耗更多的性能。可以简化动画的逻辑,减少动画涉及的元素数量,或者将复杂的动画拆分成多个简单的动画,逐个执行。例如,如果有一个包含多个子元素的容器需要进行动画,可以先对容器进行整体的动画,然后再对内部子元素进行相对简单的动画,这样可以减少回流和重绘的范围,提升性能。

通过以上这些优化方案,可以有效地减少 CSS 中的回流和重绘操作,提升页面的性能,为用户提供更流畅的浏览体验。在实际开发中,需要根据具体的项目需求和页面结构,综合运用这些方法,不断优化页面性能。