渲染中回流与重绘讲解

news2024/12/25 1:26:08

浏览器的渲染过程

本文先从浏览器的渲染过程来讲解回流与重绘。

在这里插入图片描述

从上面这个图上,我们可以看到,浏览器渲染过程如下:

  1. 解析HTML,生成DOM树(DOM Tree),解析CSS,生成CSS树(Style Rules)
  2. DOM树CSS树结合,生成渲染树(Render Tree)
  3. 根据生成的渲染树,进行回流(Layout或称呼为Reflow),得到节点的几何信息(位置大小
  4. 根据渲染树以及回流得到的几何信息,得到节点的绝对像素,进行重绘(Painting或称呼为Repaint ):
  5. 将像素发送给GPU,在页面上进行展示(Display)。

下面我们来了解下渲染过程中,重要的几个节点具体做了什么

生成渲染树(Render Tree)

在这里插入图片描述

为了构建渲染树Render Tree(),浏览器主要完成了以下工作:

  1. DOM Tree的根节点开始遍历每个可见节点。
  2. 对于每个可见的节点,找到Style Rules中对应的规则,并应用它们。
  3. 根据每个可见节点以及其对应的样式,组合生成Render Tree

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如scriptmetalink等。
  • 一些通过css进行隐藏的节点,比如display:none。注意,利用visibilityopacity隐藏的节点,还是会显示在渲染树上的。

注意:Render Tree只包含可见的节点

回流(Layout或Reflow)

前面我们通过构造Render Tree(渲染树),我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的document(根节点)开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%,如下示意图。

在这里插入图片描述

而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值(如下图)

在这里插入图片描述

重绘(Painting)

最终,我们通过构造渲染树(Render Tree)和回流(Layout)阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘(PaintingRepaint)。

既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

何时发生回流重绘

我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 页面一开始渲染的时候(这肯定避免不了)
  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重新排版,比如,滚动条出现的时候或者修改了根节点。

注意:回流一定会触发重绘,而重绘不一定会回流

浏览器的优化机制

现代的浏览器都是很聪明的,由于每次重新排版都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重新排版过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。

减少回流和重绘

好了,到了我们今天的重头戏,前面说了这么多背景和理论知识,接下来让我们谈谈如何减少回流和重绘。

由于回流和重绘可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对样式和DOM的修改,然后一次处理掉。

合并样式的修改

const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次回流。但是如果上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次回流。

因此,我们可以合并所有的改变然后一次处理,比如我们可以采取以下的方式:

  • 使用style属性cssText
const el = document.getElementById('test'); 
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;'; 
  • 修改dom属性className
const el = document.getElementById('test'); 
el.className += ' active'; // 注意class前有空格

批量修改DOM

当我们要执行一段批量插入节点的代码:

function appendDataToElement(parentElement, textArr) {
  for (let i = 0; i < textArr.length; i++) {
    const li = document.createElement('li');
    li.textContent = textArr[i];
    parentElement.appendChild(li);
  }
}

const ul = document.getElementById('list');
appendDataToElement(ul, ['文本1','文本2','文本3']);

如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中。

该过程的第一步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

我们可以让DOM脱离文档流,使用以下三种方式进行优化:

1. 隐藏元素,应用修改,重新显示

这个会在展示和隐藏节点的时候,产生两次重绘

function appendDataToElement(parentElement, textArr) {
  for (let i = 0; i < textArr.length; i++) {
    const li = document.createElement('li');
    li.textContent = textArr[i];
    parentElement.appendChild(li);
  }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, ['文本1','文本2','文本3']);
ul.style.display = 'block';

2. 使用文档片段(documentFragment)在当前DOM之外构建一个子树,再把它拷贝回文档

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, ['文本1','文本2','文本3']);
ul.appendChild(fragment);

3. 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, ['文本1','文本2','文本3']);
ul.parentNode.replaceChild(clone, ul);

对于上述情况,我写了一个demo来测试修改前和修改后的性能。然而实验结果不是很理想。

原因其实上面也说过了,浏览器会使用队列来储存多次修改,进行优化,所以对这些优化方案,我们其实不用过分考虑。

避免触发同步布局事件

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

这段代码看上去是没有什么问题,可是其实会造成很大的性能问题。在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。我们可以优化为:

const width = box.offsetWidth;
function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}

同样,我也写了个demo来比较两者的性能差异,paragraphs数量越多,差异约明显,

复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。

css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

如何使用

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

效果

我们可以先看个例子。我通过使用chrome的Performance捕获了一段时间的回流重绘情况,实际结果如下图:

在这里插入图片描述

从图中我们可以看出,在动画进行的时候,几乎没有发生回流重绘。如果感兴趣你也可以自己做下实验。

示例代码:

<div class="demo"></div>
.demo {
  position: absolute;
  width: 100px;
  height: 100px;
  background: red;
  transition: transform 10s;
}
.demo:hover {
  transform: translate(500px, 500px);
}

重点

  • 使用css3硬件加速,可以让transformopacityfilters这些动画几乎不会引起回流重绘
  • 对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

css3硬件加速的坑

  • 如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/827804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通过启动盘安装 SylixOS

通过启动盘安装 SylixOS 制作启动盘 将 U 盘插入电脑。 打开 RealEvo-IDE&#xff0c;并在菜单栏选择 Tools > RealEvo-SylixOS-Installer 启动安装工具。 设置启动盘各项参数&#xff0c;如下图红框所示。 单击一键安装 SylixOS。程序运行过程信息输出显示如下图所示。…

c语言~野指针

1、野指针&#xff0c;既没有初始化的指针&#xff0c;//如果没有给指针初始化&#xff0c;则指针p的内容为随机地址&#xff0c;会随机指向&#xff0c;故成为野指针&#xff0c;不可以操作野指针 #include "stdio.h" #include <stdlib.h>int main() {//1、野…

Nginx之正、反向代理

什么是代理 A同学在Ai大时代背景下开启他的创业之路&#xff0c;目前他遇到的最大的一个问题就是启动资金&#xff0c;于是他决定去找马云爸爸借钱&#xff0c;可想而知&#xff0c;最后碰一鼻子灰回来了&#xff0c;情急之下&#xff0c;他想到一个办法&#xff0c;找关系开后…

Ubuntu20.04之VNC的安装与使用

本教程适用于Ubuntu20.04及以下版本&#xff0c;Ubuntu22.04版本或有出入 更多更新的文章详见我的个人博客&#xff1a;【前往】 文章目录 1.安装图形桌面1.1选择安装gnome桌面1.2选择安装xface桌面 2.安装VNC-Server3.配置VCN-Server4.连接VNC5.设置VNC-Server为系统服务&…

STM32 DMA

DMA介绍 DMA&#xff0c;Direct Memory Access&#xff0c;即直接存储器访问。 DMA传输&#xff0c;将数据从一个地址空间复制到另一个地址空间。&#xff08;内存&#xff08;程序里定义的数组&#xff09;->外设&#xff08;串口、SPI等外设的数据寄存器&#xff09;、外…

web集群学习--静态网页和动态网页的区别、WEB1.0和WEB2.0的区别、Tomcat安装以及部署jpress应用

1.静态网页和动态网页的区别 1.1概念 静态网页&#xff1a; 由在服务器上提前创建好的HTML文件组成&#xff0c;它的内容在用户请求页面时不会发生改变。当用户访问一个静态网页时&#xff0c;服务器会直接将预先准备好的HTML文件发送给用户的浏览器进行显示。因为静态网页的…

iptables防火墙、filter表控制、扩展匹配、使用iptables配置网络型防火墙、NAT原理、配置SNAT

day05 day05iptables防火墙filter表filter中的三条链环境准备iptables操作验证FORWARD链准备环境配置FORWARD链NAT配置SNAT iptables iptables有多种功能&#xff0c;每一种功能都用一张表来实现最常用的功能是防火墙和NAT从RHEL7开始&#xff0c;默认的防火墙为firewalld&a…

Pandas时序数据分析实践—时序数据集

1. 跑步运动为例&#xff0c;对运动进行时序分析 时序数据是指时间序列数据&#xff0c;是按照时间顺序排列的数据集合&#xff0c;每个数据点都与一个特定的时间戳相关联。在跑步活动中&#xff0c;我们可以将每次跑步的数据记录作为一个时序数据样本&#xff0c;每个样本都包…

C语言隐式类型转换规则 (比较实用)

C语言隐式类型转换规则 语言规定&#xff0c;不同类型的数据需要转换成同一类型后才可进行计算&#xff0c;在整型、实型和 字符型数据之间通过类型转换便可以进行混合运算&#xff08;但不是所有类型之间都可以进 行转换) 当混合不同类型的变量进行计算时&#xff0c;便可能会…

网络故障监测终端的网络稳定性和可靠性

RTU5028E网络故障监测终端是一款功能强大且方便实用的设备&#xff0c;集合了断网、断电、网线故障报警功能。它支持同时监测多达7台网络设备&#xff0c;可以帮助用户快速定位远程网络设备离线的原因。此外&#xff0c;它还具备自动重启和远程重启网络设备的功能&#xff0c;为…

P3372 【模板】线段树 1 树状数组

题目 思路 第一眼&#xff1a;树状数组暴力&#xff0c;区间和直接用前缀和做 好&#xff0c;70分 看来需要用数学推亿推了 树状数组的区间查询&#xff1a;查分 设 c 1 a 1 , c 2 a 2 − a 1 , c 3 a 3 − a 2 . . . c i a i − a i − 1 c_1a_1,c_2a_2-a_1,c_3a_3-a_2..…

echart常用图表配置

echart常用图表配置 柱状图3D柱状图效果代码 排行榜柱状图效果代码 排行榜反转柱状图效果代码 柱状图 3D柱状图 效果 代码 import { graphic } from echartsconst VALUE Array.from({ length: 24 }, () > Math.floor(Math.random() * (5000 - 1000)) 1000)const CubeLe…

Python:列表(list)与元组(tuple)

列表与元组 列表&#xff1a;list元组&#xff1a;tuple 比较直观的区分&#xff1a;列表是中括号"[ ]“&#xff0c;元组是小括号”( )"元组可以看成列表的只读形式 # 列表 list1 [hello, world] list2 [1, 2, 3, 4, 5] list3 ["a", "b", &…

面试必考精华版Leetcode2130.链表最大孪生和

题目&#xff1a; 代码&#xff08;首刷看解析 day22&#xff09;&#xff1a; class Solution { public:int pairSum(ListNode* head) {ListNode* slowhead;ListNode* fasthead->next;while(fast->next!nullptr){slowslow->next;fastfast->next->next;}//反转…

二、韦伯定律

二、韦伯定律 定义&#xff1a;即感觉的差别阈限跟随原来刺激量的变化而变化&#xff0c;而且变现为一定的规律性&#xff0c;用公式来表示&#xff0c;就是就是△Φ/ΦC&#xff0c;其中Φ为原刺激量&#xff0c;△Φ为此时的差别阈限&#xff0c;C为常数&#xff0c;又称为韦…

C语言的转义字符

转义字符也叫转移序列&#xff0c;包含如下&#xff1a; 转移序列 \0oo 和 \xhh 是 ASCII 码的特殊表示。 八进制数示例&#xff1a; 代码&#xff1a; #include<stdio.h> int main(void) {char beep\007;printf("%c\n",beep);return 0; }结果&#xff1a; …

腾讯云-宝塔Linux面板首次登陆操作 (精简教程)

购买服务器这里就略过了... 1. 腾讯云-服务器 2. 找到你的服务器, 点击进去 3. 概要 4. 应用管理 1. 面板首页地址 http://ip:面板端口/tencentcloud2. 面板端口 默认为8888&#xff0c;您可以在登录面板后修改面板端口( 提示&#xff1a;请前往防火墙页面放行面板端口)3. 用户…

“数智新应用”不再是口号,看汽车、医药、制造企业如何突出重围?

近日&#xff0c;以“释放数智生产力”为主题的 Kyligence 用户大会在上海前滩香格里拉大酒店成功举行。大会包含上午的主论坛和下午的 4 场平行论坛&#xff0c;并举办了闭门会议、Open Day 等活动。来自金融、零售、制造、医药等行业的客户及合作伙伴带来了超过 23 场主题演讲…

SpringCloud Gateway获取请求响应body大小

前提 本文获取请求、响应body大小方法的前提 : 网关只做转发逻辑&#xff0c;不修改请求、相应的body内容。 SpringCloud Gateway内部的机制类似下图&#xff0c;HttpServer&#xff08;也就是NettyServer&#xff09;接收外部的请求&#xff0c;在Gateway内部请求将会通过Htt…

【第一阶段】kotlin语言引用数据类型

Java语言中有两种数据类型 第一种&#xff1a;基本数据类型 如int double等 第二种&#xff1a;引用数据类型。如String kotlin只有一种数据类型&#xff0c;看起来都是引用数据类型&#xff0c;实际上编译器会在Java字节码中&#xff0c;修改成基本类型 //Java语言中有两种数…