深入理解回流与重绘:优化网页性能的关键

0 阅读6分钟

在前端开发中,网页性能优化是一个永恒的话题。其中,回流(Reflow)和重绘(Repaint)是影响网页性能的重要因素。理解它们的原理和触发机制,能够帮助我们编写出性能更优的代码。

布局的难点与 BFC/FFC

在网页布局中,列式布局和理解 BFC/FFC 是一大难点。BFC 即 Block Formatting Context,中文名为块级格式化上下文。它是页面中的一块渲染区域,有一套渲染规则来约束块级盒子的布局,并且与这个区域外部毫不相干。

BFC 的基本概念

HTML 根元素是最外层的第一个 BFC 元素。在 BFC 中,块级元素从上到下排列,行内元素从左到右排列,这就形成了我们所说的文档流。例如,以下简单的 HTML 代码:

<!DOCTYPE html>
<html>
<head>
    <title>BFC 示例</title>
</head>
<body>
    <div style="background-color: lightblue;">块级元素 1</div>
    <div style="background-color: lightgreen;">块级元素 2</div>
    <span>行内元素 1</span>
    <span>行内元素 2</span>
</body>
</html>

在这个例子中,div 元素作为块级元素,会从上到下排列;而 span 元素作为行内元素,会从左到右排列。

触发 BFC 的方式

有多种方式可以触发 BFC,例如 float 属性不为 noneoverflow 属性不为 visibledisplay 属性为 flex 等。以下是一个使用 overflow: hidden 触发 BFC 的示例:

<!DOCTYPE html>
<html>
<head>
    <title>BFC 触发示例</title>
    <style>
        .parent {
            overflow: hidden;
            background-color: lightblue;
        }
        .child {
            float: left;
            width: 100px;
            height: 100px;
            background-color: lightgreen;
        }
    </style>
</head>
<body>
    <div class="parent">
        <div class="child"></div>
    </div>
</body>
</html>

在这个例子中,父元素 parent 使用 overflow: hidden 触发了 BFC,从而包含了浮动的子元素 child

列式布局与 table 标签

在早期的网页布局中,table 标签常被用于列式布局。然而,如今这种方式已经不太推荐使用了,主要原因有以下几点:

  1. 触发太多的回流和重绘table 布局中局部的改变会影响整个 table 的回流重排,性能开销较大。
  2. 语义不符table 标签主要用于展示数据表,用它来做布局不符合语义化的要求。
  3. 不够灵活table 布局的结构相对固定,不够灵活,难以适应不同的屏幕尺寸和布局需求。

回流与重绘的基本概念

回流(Reflow)

回流,也称为重排,是指当 RenderTree 中部分或全部元素的尺寸、结构,或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程。例如,当我们改变元素的宽度、高度、位置等属性时,就会触发回流。

<!DOCTYPE html>
<html>
<head>
    <title>回流示例</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: lightblue;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <button onclick="changeWidth()">改变宽度</button>
    <script>
        function changeWidth() {
            const box = document.getElementById('box');
            box.style.width = '200px';
        }
    </script>
</body>
</html>

在这个例子中,点击按钮会改变 box 元素的宽度,从而触发回流。

重绘(Repaint)

重绘是指当页面元素样式的改变并不影响它在文档流中的位置时,浏览器重新绘制元素的过程。例如,改变元素的颜色、背景色、可见性等属性,只会触发重绘,不会触发回流。

<!DOCTYPE html>
<html>
<head>
    <title>重绘示例</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: lightblue;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <button onclick="changeColor()">改变颜色</button>
    <script>
        function changeColor() {
            const box = document.getElementById('box');
            box.style.backgroundColor = 'lightgreen';
        }
    </script>
</body>
</html>

在这个例子中,点击按钮会改变 box 元素的背景色,从而触发重绘。

触发回流的方式

回流是一个比较昂贵的操作,因为它需要重新计算元素的布局和位置。以下是一些常见的触发回流的方式:

页面首次渲染

页面首次渲染是最耗时的回流操作,因为它需要从无到有地构建整个页面的布局。据统计,网页每满 0.1s 加载时间可能会少 1000 万用户,因此优化首次渲染性能非常重要。

浏览器窗口的大小改变

当用户调整浏览器窗口的大小时,页面中的元素布局可能会发生变化,从而触发回流。为了减少这种情况下的性能开销,可以使用 resize 事件的防抖函数。

function debounce(func, delay) {
    let timer = null;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

window.addEventListener('resize', debounce(() => {
    // 处理窗口大小改变的逻辑
}, 200));

元素尺寸或位置发生改变

当元素的尺寸或位置发生改变时,如修改 widthheightmarginpadding 等属性,会触发回流。不过,transtiontransformopacity 等属性在新图层中不会触发回流,因此可以利用这些属性来实现动画效果,提高性能。

元素内容改变

当元素的文本内容、图片大小、列表项增加或删除时,也会触发回流。例如,使用 appendChild()removeChild() 方法操作 DOM 元素时,就会触发回流。

display 属性的改变

当元素的 display 属性从 none 变为 block 或从 block 变为 none 时,会触发回流。因为 display: none 的元素不会参与布局,而 display: block 的元素会参与布局。

字体大小的变化

修改元素的字体大小会影响元素的布局,从而触发回流。因此,在实际开发中,尽量避免频繁修改字体大小。

激活 CSS 伪类

当激活 CSS 伪类,如 :hover 时,浏览器需要重新计算元素的样式和布局,可能会触发回流。例如:

<!DOCTYPE html>
<html>
<head>
    <title>伪类触发回流示例</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: lightblue;
        }
        #box:hover {
            width: 200px;
        }
    </style>
</head>
<body>
    <div id="box"></div>
</body>
</html>

查询某些属性或调用某些方法

当查询某些属性或调用某些方法时,如 img.getBoundingClientRect(),浏览器为了返回最新的布局信息,会强制触发回流。因此,尽量避免在频繁的循环中查询这些属性或调用这些方法。

性能优化建议

了解了回流和重绘的原理和触发机制后,我们可以采取一些措施来优化网页性能:

减少回流和重绘的次数

尽量批量修改元素的样式,而不是一次修改一个样式。例如,使用 class 来修改元素的样式:

<!DOCTYPE html>
<html>
<head>
    <title>批量修改样式示例</title>
    <style>
        .new-style {
            width: 200px;
            height: 200px;
            background-color: lightgreen;
        }
    </style>
</head>
<body>
    <div id="box"></div>
    <button onclick="changeStyle()">修改样式</button>
    <script>
        function changeStyle() {
            const box = document.getElementById('box');
            box.classList.add('new-style');
        }
    </script>
</body>
</html>

使用 display: none 进行批量操作

如果需要对一个元素进行多次修改,可以先将其设置为 display: none,进行批量修改后再将其显示出来。因为 display: none 的元素不会参与布局,所以不会触发回流和重绘。

使用 transform 实现动画效果

transform 属性在新图层中不会触发回流,因此可以利用它来实现动画效果,提高性能。例如:

<!DOCTYPE html>
<html>
<head>
    <title>transform 动画示例</title>
    <style>
        #box {
            width: 100px;
            height: 100px;
            background-color: lightblue;
            transition: transform 1s;
        }
        #box:hover {
            transform: translateX(100px);
        }
    </style>
</head>
<body>
    <div id="box"></div>
</body>
</html>

总结

回流和重绘是影响网页性能的重要因素,理解它们的原理和触发机制,能够帮助我们编写出性能更优的代码。在实际开发中,我们应该尽量减少回流和重绘的次数,合理使用 transform 等属性,优化网页的渲染性能。希望本文能够帮助你更好地理解回流和重绘,提升你的前端开发技能。