浏览器渲染过程与性能优化:回流与重绘

152 阅读7分钟

前言

大家好!在网页开发过程中,理解浏览器如何加载和显示页面内容是非常重要的。之前我们简单聊过一下从输入url到显示页面,浏览器都干了些啥,今天我们将深入探讨浏览器的渲染流程,特别是回流(Reflow)与重绘(Repaint)的概念,以及它们对性能的影响。这不仅对我们项目的开发中的性能优化有很大帮助,同时也是一道经典的面试题。通过了解这些概念及其触发条件,我们可以采取措施来减少不必要的回流和重绘,从而提高网页的响应速度和用户体验。

正文

浏览器加载资源后的渲染流程

当浏览器接收到一个HTML文档时,它会经历一系列步骤来解析并最终显示页面内容:

  1. 解析HTML代码:浏览器首先读取HTML文档,根据html文档的第一行去判断文档类型是否为html,并根据标签结构生成DOM树。这个其实和昨天聊到的虚拟dom被编译之后产生的树型对象差不多,感兴趣的读者姥爷可以去看看key:我与diff之间“说得清道得明”的关系 - 掘金

  2. 解析CSS:接着,浏览器解析所有相关的CSS样式表,生成CSSOM(CSS Object Model)树。

  3. 构建Render Tree:将DOM树和CSSOM树结合,去除不可见元素(如display: none;的元素),生成渲染树(Render Tree)。

  4. 计算布局(回流):根据渲染树进行布局计算,确定每个节点的具体位置和大小。

  5. 绘制页面(重绘):最后,GPU使用布局信息绘制页面,使用户能够看到最终结果。

什么是回流?

定义: 回流是指浏览器为了重新计算页面布局而执行的过程。这是一个相对昂贵的操作,因为它涉及到大量的计算。每当页面中的某个部分影响到布局时,浏览器就需要重新计算受影响区域内的所有元素的位置和尺寸。

触发因素:

  • 改变窗口尺寸

  • 改变元素尺寸(例如,设置宽度、高度)

  • 增加或删除可见元素

  • 页面初次渲染

  • 修改元素的字体大小

  • 添加或删除样式规则,尤其是那些影响布局的规则

每次发生回流时,浏览器都需要重新计算受影响区域内的所有元素的位置和尺寸,这可能会导致整个页面的重新布局。回流通常是一个耗时的操作,尤其是在复杂的布局中。

 

什么是重绘?

定义: 重绘是指将已经计算好布局的元素在屏幕上展示出来的过程。相比回流而言,重绘通常消耗较少的资源。重绘只是视觉效果的变化,不会影响到布局。

触发因素:

  • 修改背景色

  • 更换图片

  • 修改颜色

  • 设置透明度

  • 修改边框样式

值得注意的是,回流是一定一定会导致重绘,因为一旦布局发生变化,页面上的视觉表现也需要随之更新;但是,重绘并不一定会引发回流,如果仅仅是颜色或背景图等非几何属性的变化,则不会影响到布局。这里我也给大家准备了一个小demo,帮助大家理解回流重绘的概念。

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>回流与重绘</title>
    <style>
        .Reflow {
            width: 100px;
            height: 100px;
            background-color: red;
            font-size: 15px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .Repaint {
            width: 80px;
            height: 80px;
            background-color: blue;
            font-size: 15px;
            display: flex;
            align-items: center;
            justify-content: center;
            border: 10px solid green;
        }
    </style>
</head>

<body>
    <div class="Reflow">回流</div>
    <div class="Repaint">重绘</div>
    <button id="ReflowBtn">触发回流</button>
    <button id="ReRepaintBtn">触发重绘</button>

    <script>
        const ReflowBtn = document.getElementById('ReflowBtn')
        const RepaintBtn = document.getElementById('ReRepaintBtn')

        ReflowBtn.onclick = function () {
            const div = document.querySelector('.Reflow')
            const width = div.style.width
            const height = div.style.height
            const fontSize = div.style.fontSize
            div.style.fontSize = fontSize === '15px' ? '30px' : '15px'
            div.style.height = height === '100px' ? '200px' : '100px'
            div.style.width = width === '100px' ? '200px' : '100px'
        }

        RepaintBtn.onclick = function () {
            const div = document.querySelector('.Repaint')
            console.log(div);

            const color = div.style.backgroundColor
            const opacity = div.style.opacity
            const boredr = div.style.border
            div.style.backgroundColor = color === 'blue' ? 'yellow' : 'blue'
            div.style.opacity = opacity === '1' ? '0.5' : '1'
            div.style.border = boredr === '10px solid rgb(0, 255, 47)' ? '10px solid blue' : '10px solid rgb(0,255, 47)'
        }
    </script>
</body>

</html>

1.png

如何减少回流/重绘

既然无论是回流还是重绘都会导致浏览器的性能消耗,而其中回流消耗的性能又明显要大于重绘。所以为了提升性能,我们需要尽量减少不必要的回流尤其是重绘操作。以下是一些有效的方法:

  • 合理利用浏览器的优化策略:现代浏览器都具备一定的优化机制,比如渲染队列管理,可以批量处理多次样式更改请求,以减少实际发生的回流次数。
element.style.width = '100px';
element.style.height = '200px';
element.style.padding = '10px';

这段代码会触发三次回流,因为每次设置样式都会导致布局计算。但通过渲染队列机制,浏览器可能会将这三次样式更改合并成一次回流操作。

  • 隐藏元素后再修改样式:对于需要大量样式修改的情况,可以先将该元素设置为display: none,完成所有改动后再将其设为可见,这样可以避免中间状态下的多次回流。

 var element = document.getElementById('myElement');
    // 首先设置为不可见
    element.style.display = 'none';

    // 修改元素的样式
    element.style.width = '300px';
    element.style.height = '300px';
    element.style.backgroundColor = 'lightgreen';
    element.style.borderRadius = '15px';  // 添加圆角
    element.style.boxShadow = '5px 5px 15px rgba(0,0,0,0.3)';  // 添加阴影

    // 最后设置回可见
    setTimeout(function() {
      element.style.display = 'block';
    }, 0); 
    // 使用setTimeout将显示为block设置为宏任务,确保样式更新完成后再显示

  • 使用 transform 和 opacity 进行动画:使用 transform 属性进行位移、旋转等变换,以及 opacity 属性控制透明度,都可以直接由GPU加速,而不引起回流。

  • 避免频繁读取布局信息:获取某些布局信息(如 offsetWidth、offsetHeight 等)会强制浏览器立即执行当前的渲染队列。如果可能,尽量缓存这些值而不是反复读取。

  • 减少复杂的选择器:过于复杂的选择器会增加浏览器匹配样式的时间,从而可能导致更多的回流。保持选择器简单可以帮助减少这一开销。

  • 使用虚拟DOM:如果使用了像React或Vue这样的框架,那么它们内置的虚拟DOM机制可以在一定程度上帮助减少实际的DOM操作,从而减少回流和重绘。在我昨天的文章中有关于虚拟dom的详细介绍,有需要的可以看看。

  • vue中的v-show与v-if:这也是一个老生常谈的话题了,v-show本质上是控制元素的display属性,不论是否显示,dom都会占据文档流,但是v-if则是直接控制元素是否被添加到dom树中。所以在需要频繁切换是否显示的元素中是更推荐v-show去避免对dom树进行频繁操作的。

回流的强制执行

某些属性的获取操作,如offsetWidth、offsetHeight、offsetTop、offsetLeft、clientWidth、clientHeight、clientTop、clientLeft及scroll一系列属性,会强制浏览器立即执行当前的渲染队列。这意味着即使是在队列中等待的回流任务也会被立刻处理。所以在css中必须谨慎使用这类属性,尤其是在性能敏感的应用场景中。

结语

通过对浏览器渲染过程的理解,我们能够更加有效地识别和解决性能瓶颈。通过最小化回流和重绘的发生,我们可以显著提升网页的加载速度和流畅度。希望本文能帮助你更好地掌握相关知识,祝各位读者姥爷开发愉快,0 warning(s), 0 error(s)!