性能优化必解决——重绘与回流

852 阅读5分钟

想必大家写简单的页面早已经是得心应手了,但是如果写的页面要考虑性能消耗,那估计会难到很多初学者,在网页开发中,优化性能是非常重要的,如果写的网页一次性加载大量数据可能会出现一直卡住等现象。浏览器渲染过程中的回流(reflow)和重绘(repaint)是影响性能的关键环节。因此,本文将深入讲解重排和重绘的概念及其底层原理,并提供优化策略和代码示例。

6b1f866f1426663098670d32566590a80fe9b6281a910d-Edtcck_fw658.webp

浏览器渲染流程

在理解重排和重绘之前,我们需要了解浏览器渲染页面的流程。浏览器渲染页面通常包括以下几个步骤:

  1. 解析 HTML:将 HTML 转换为 DOM 树(Document Object Model)。
  2. 解析 CSS:将 CSS 转换为 CSSOM 树(CSS Object Model)。
  3. 生成渲染树:将 DOM 树和 CSSOM 树合并生成渲染树(Render Tree)。渲染树只包含可见元素,并且每个元素包含样式信息。
  4. 布局(Layout) :计算每个元素的位置和大小,生成布局树(Layout Tree)。
  5. 分层与图层合成:将布局树分层,生成多个图层,并合成最终的图层树。
  6. 绘制(Painting) :遍历图层树,绘制每个图层的内容到屏幕上。

布局与分层

布局阶段会计算每个元素的几何属性(位置和大小),生成布局树。布局树和渲染树类似,但包含几何信息。分层阶段将布局树中的元素分成多个图层,以便进行复杂的效果(如 3D 变换、z-index)。

图层合成与绘制

图层合成阶段将不同图层合成一棵图层树,然后在绘制阶段遍历图层树,将每个图层的内容绘制到屏幕上。

什么是回流(Reflow)?

回流,也称为重排(reflow),是指当网页布局信息发生改变时,浏览器重新计算元素的位置和大小的过程。例如,增加或减少元素、改变元素的尺寸、修改布局属性(如 marginpadding)等,都会触发重排。

回流示例

以下是一个简单的示例触发回流:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reflow Example</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <button onclick="triggerReflow()">Trigger Reflow</button>
    <script>
        function triggerReflow() {
            var box = document.getElementById('box');
            box.style.width = '200px'; // 修改宽度会触发重排
        }
    </script>
</body>
</html>

ReflowExample2--MicrosoftEdge2024-08-0700-14-15-ezgif.com-video-to-gif-converter.gif

可以看到,当我们点击按钮将宽度修改了,就是改变了元素的布局,这样就是触发了回流,而且触发了回流就一定也会触发重绘。

什么是重绘(Repaint)?

重绘是指当元素的外观(例如颜色、背景图片)发生改变时,浏览器重新绘制元素的过程。重绘不涉及布局计算,因此相比重排开销较小。

重绘示例

以下是一个简单的示例触发重绘:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Repaint Example</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: red;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <button onclick="triggerRepaint()">Trigger Repaint</button>
    <script>
        function triggerRepaint() {
            var box = document.getElementById('box');
            box.style.backgroundColor = 'blue'; // 修改背景颜色会触发重绘
        }
    </script>
</body>
</html>

RepaintExample2--MicrosoftEdge2024-08-0700-14-58-ezgif.com-video-to-gif-converter.gif

当改变颜色后,就是触发了重绘效果,但是重绘只改变了元素的外观,并没有改变布局,所以触发了重绘不一定会回流。

回流与重绘的关系

这里总结一下两者的关系。重排一定会导致重绘,因为布局改变了,元素的外观也会随之改变。 重绘不一定会导致重排,因为有些情况下,元素的位置和大小并没有改变,只是外观发生了变化。

优化策略

避免频繁的样式修改

频繁修改样式会导致频繁的重排和重绘,从而影响性能。可以通过减少样式修改的次数来优化性能。例如,不要直接修改样式属性,而是通过切换类名来修改样式:

// 不推荐
element.style.width = '200px';
element.style.height = '200px';

// 推荐
element.classList.add('new-class');

使用 display: none 暂时移除 DOM 元素

在对元素进行多次操作之前,可以先将其从 DOM 树中移除,操作完成后再将其添加回去:

// 移除元素
element.style.display = 'none';

// 进行多次操作
element.style.width = '200px';
element.style.height = '200px';

// 添加回 DOM
element.style.display = 'block';

使用文档碎片减少 DOM 操作

通过使用文档碎片(DocumentFragment),可以将多次 DOM 操作合并为一次,从而减少重排和重绘的次数:

var fragment = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
    var newElement = document.createElement('div');
    newElement.textContent = 'Item ' + i;
    fragment.appendChild(newElement);
}
document.body.appendChild(fragment);

优化 CSS 动画

尽量使用 CSS 动画而不是 JavaScript 来实现动画效果,因为浏览器对 CSS 动画进行了优化:

/* 使用 CSS 动画 */
@keyframes slidein {
    from {
        transform: translateX(0);
    }
    to {
        transform: translateX(100px);
    }
}

.element {
    animation: slidein 1s forwards;
}

应用场景

当我们有大量数据要渲染,例如几万条及以上时,我们如果直接全放在一个页面去渲染,不仅会消耗很多性能,也会增加渲染时间。在这种情况下,我们可以采用如下策略:

1.使用文档碎片

var fragment = document.createDocumentFragment();
for (var i = 0; i < 10000; i++) {
    var newElement = document.createElement('div');
    newElement.textContent = 'Item ' + i;
    fragment.appendChild(newElement);
}
document.body.appendChild(fragment);

2.使用 requestAnimationFrame 替代 setTimeout

var count = 0;
function renderItems() {
    if (count < 10000) {
        var newElement = document.createElement('div');
        newElement.textContent = 'Item ' + count++;
        document.body.appendChild(newElement);
        requestAnimationFrame(renderItems);
    }
}
requestAnimationFrame(renderItems);

3.减少布局触发

避免频繁调用 offsetWidthoffsetHeightgetBoundingClientRect 等会触发重排的属性。因为重拍的性能消耗比较大。

4.使用懒加载: 对于大量图片或资源,使用懒加载技术:

<img src="image.jpg" loading="lazy" alt="Lazy Loaded Image">

或使用第三方库如 lozad.js

<script src="https://cdn.jsdelivr.net/npm/lozad@1.14.0/dist/lozad.min.js"></script>
<script>
    const observer = lozad(); 
    observer.observe();
</script>
<img class="lozad" data-src="image.jpg" alt="Lazy Loaded Image">

通过应用这些优化策略,可以有效减少重排和重绘对性能的影响,从而提升网页的渲染速度和用户体验。

总结

通过深入理解浏览器渲染过程中的重排和重绘,我们可以采取相应的优化策略,减少不必要的渲染开销,提升网页性能。无论是通过减少样式修改、使用文档碎片、优化 CSS 动画,还是通过懒加载优化大数据渲染,这些方法都能帮助我们编写更高效的代码,提供更流畅的用户体验。希望本文的讲解和示例能够帮助大家更好地理解和优化网页性能。