浏览器的页面渲染

前言

当咱们在浏览器中输入一段url地址栏时会发生什么呢?它会经过两个过程,一个是浏览器向后端服务器发送请求获取数据,还有一个则是浏览器本身的渲染过程。今天小编就带大家主要来聊一聊浏览器的渲染过程。

正文

浏览器的渲染过程

当浏览器拿到请求过来的资源和数据之后,就会进行渲染了。首先浏览器会从得到的资源中解析出html文件,css文件,但是此时这些文件都不是真正意义上咱们所说的html文件,css文件,这是因为在网络数据传输过程中,传输的都是二进制流,所以咱们需要将二进制的字节数据解析为字符串才行。得到字符串之后,浏览器会对字符串打上一个Toekn标记,标记完之后就会将字符串数据变成一个个的Node节点,然后再构建DOM树,将css文件构成CSSOM树。这时候就有两个树了,浏览器就会拿着这两颗树重新去生成一颗rander树,这里有一个需要注意的地方,渲染树只会包含显示的节点,所以当一段html结构被设置加上了display:none这个属性后,这个元素或者结构就不会被包含在渲染树上。

接着浏览器就会拿着这颗渲染树去做计算布局也叫做回流,就是计算元素的位置和大小过程,然后开始绘制也就是咱们所说的重绘,就是当元素的像背景、颜色、边框样式等发生变化是,浏览器就重新绘制元素的过程。

大概过程如下:

1、解析html代码,生成一个dom树

2、解析css代码,生成CSSOM树

3、将dom树和CSSOM树结合,去除不可见的元素,生成Render Tree

4、计算布局,(回流 | 重排)根据Render Tree进行布局计算,得到每一个节点的几何信息

5、绘制页面,(重绘)GPU根据布局信息绘制

为了方便大家理解树,这里写一个简版的dom树,用js模拟下,(为了帮助理解)

python 复制代码
<div id="app">
    <div class="name">Tom</div>
    <div class="age">18</div>
</div>

// 上面的这段html理解成树大致如下:

let node = {
    tag: 'div',
    id: 'app',
    class: '',
    x: 0,
    y: 100,
    children: [
        {
            tag: 'div',
            id: '',
            class: 'name',
            text: 'Tom',
            ......
        },
        {
            tag: 'div',
            id: '',
            class: 'age',
            text: '18',
            ......
        }
    ]
}

为何要转换成dom树?

屏幕的物理发光点想要发光就必须清楚在哪里发光,也就是结构,转换成树结构就方便去绘制页面,清楚这个了你就会理解转换成CSSOM树的意义,这样就方便绘制颜色等等

我们主要来聊一聊第四个和第五个过程,计算布局我们称之为回流 或者重排 ,绘制页面我们称之为重绘

前三个步骤无法进行优化,所以我们单独领出来后两个步骤来聊聊

回流

浏览器计算页面布局的过程就叫做回流

只要页面有容器几何信息发生变更就会发生回流,也就是影响了它的排版,所以回流太常见了,有以下几种:

1、页面初次渲染

2、增加删除可见的DOM元素

3、改变元素的几何信息(位置大小边框)

4、窗口大小改变

5、改变字体的大小是会回流的

容器脱离文档流是不会发生回流的,当然这是针对影响其他元素而言,比如你增删一个可见元素,是会影响下面容器的几何信息的。对于脱离文档流容器本身而言,肯定是发生回流的

当回流发生时,浏览器会重新构建渲染树,并基于新的渲染树重新绘制受影响的区域。回流是一个比较昂贵的操作,因为它涉及到复杂的布局计算,特别是当大量元素需要重新布局时。

重绘

GPU将已经计算好几何信息的容器在屏幕上亮起来就是重绘

所以只要元素的非几何属性发生变化时,就会发生重绘

  1. 改变元素的颜色或背景。(修改背景颜色、修改背景图片、边框颜色

    字体颜色)

  2. 更新元素的边框颜色或样式。

  3. 切换透明度,但不改变元素的位置或大小。

重绘比回流成本低,因为它并不需要重新计算布局,仅需要更新像素颜色。然而,频繁的重绘仍然可能导致性能问题,尤其是在动画效果中。

注意,既然发生了回流,就一定会带来重绘,重绘不一定带来回流

如何减少回流、重绘

咱们来看这个例子

ini 复制代码
<div id="app">Hello</div>

<script>
    let app = document.getElementById('app')

    app.style.position = 'relative';
    app.style.width = '100px'
    app.style.height = '200px'
    app.style.left = '10px'
    app.style.top = '10px'
</script>

请问以上这段js代码执行会有几次回流?(不算浏览器初次加载)

发生四次回流,因为只有四行代码影响了几何信息。没错,以前老版本浏览器是这样的,现在浏览器执行这个只会发生一次回流。

老版本浏览器一般为了减少回流就会这样写

css 复制代码
app.style.cssText = 'width: 100px; height: 200px; left: 10px; top: 10px'

放在一起执行就相当于一次性回流所有,或者也可以动态绑定一个类名,发生某个事件的时候给类名放上去,这样也是一次回流

性能优化

为了避免过多的回流和重绘,可以采用以下几种策略:

  • 批量更改:尽可能地将多个样式更改操作合并成一次,减少回流和重绘次数。
  • 使用CSS3 Transform和Opacity:这些属性的更改通常只会触发重绘而不是回流。
  • 利用requestAnimationFrame:确保动画在下一次重绘之前进行计算,避免不必要的帧浪费。
  • 将动画或频繁变化的元素放在独立的层上,如使用position: absolute;transform: translateZ(0);

浏览器的优化策略

相比较以前的浏览器,现在的浏览器是有一个优化策略的,它执行js的时候会维护一个渲染队列,改变一个容器的样式,导致需要发生回流的时候,这个操作会进入渲染队列,如果还有相同行为,继续进入队列,直到下面没有样式修改,浏览器会批量化地执行渲染队列中的回流过程,这只发生一次回流

换个例子,咱们修改一下上面的例子,每个样式下面都将属性值打印出来,这样会有几次回流

ini 复制代码
app.style.width = '100px'
console.log(app.offsetWidth) 
app.style.height = '200px'
console.log(app.offsetHeight)
app.style.left = '10px'
console.log(app.offsetLeft)
app.style.top = '10px'
console.log(app.offsetTop)

offsetWidth就是读取容器的宽度,clientWidth也是读取容器的高度,不过前者会包含边框,innerWidth是获取window的宽度

这种情况浏览器会发生四次回流,这些代码都是同步代码,因此从上往下执行,执行打印语句的时候无法跳过它,并且当你要读取几何信息的时候是会打断它入队列的,因为读取几何信息就是一个计算布局,也就是回流或重绘,重绘是非常昂贵的,因此浏览器需要专注此时的重绘,所以它会暂停渲染队列的任务,等待重绘完毕后再去执行此时的渲染队列,因此碰到计算布局的时候一定会带来一次渲染队列的执行或刷新,也就是带来一次回流

浏览器会维护一个渲染队列,当改变元素的几何属性导致回流发生时,回流行为会被加载到渲染队列中,在达到阈值或者一定时间之后会一次性将渲染队列中所有的回流生效

offsetTop,offsetLeft,offsetWidth,offsetHeight//读得到边框

clientTop,clientLeft,clientWidth,clientHeight//读不到边框

scrollTop,scrollLeft,scrollWidth,scrollHeight

这些会强制刷新渲染队列,有可能导致浏览器回流,遇到这些就把渲染队列刷新执行回流一次,否则加进回流队列当今浏览器都有这个优化机制

所以上面的那个写法,咱们可以把所有读取几何信息的方法全部丢到后面去,这样就是一次回流了

ini 复制代码
app.style.width = '100px'
app.style.height = '200px'
app.style.left = '10px'
app.style.top = '10px'

console.log(app.offsetLeft)
console.log(app.offsetHeight)
console.log(app.offsetWidth) 
console.log(app.offsetTop)

前面那些修改几何属性都会把他们加进回流队列里,执行第一个打印语句的时候,就执行一次回流队列,渲染队列就空了,空了的队列无法引起回流,读取值是不引起回流的,读取值引起的执行队列才会引起回流

所以说,我们为了减少回流,重绘,可以合理利用浏览器的优化策略,少去读取几何信息,或者统一放到最后读取

代码的优化

给你一段代码来进行优化

ini 复制代码
<ul id="box"></ul>

<script>
    const ul = document.getElementById("box");
    
    for (let i = 1; i<=100; i++) {
        let li = document.createElement("li");
        let text = document.createTextNode(i) // 创建文本节点
        li.appendChild(text) // 这个不算回流,因为text添加到li上时,li还没出现在页面上
        ul.appendChild(li);  
    }
</script>

这段代码就是循环创建100个li,并且每个li都有文本,这其实就是新建一个元素,每次新建一个就是回流一次,不算页面初次加载就是100次回流。

none、block优化

咱们还可以让需要修改几何属性的容器先脱离文档流不显示,修改完几何属性之后再显示。刚开始咱们说过,将一段元素设置为display:none时,他就不会再渲染树中出现了,所以咱们可以这样干

ini 复制代码
<script>
    const ul = document.getElementById("box");
    ul.style.display = "none"; 
    
    for (let i = 1; i<=100; i++) {
        let li = document.createElement("li");
        let text = document.createTextNode(i) // 创建文本节点
        li.appendChild(text) 
        ul.appendChild(li);  // 100次
    }
    ul.style.display = "block"  
</script>

将一个元素display: none;后统一修改其样式,然后再将其display: block;回来。这样就是发生两次回流,none,block分别一次。

Fragment文档碎片优化

Fragment是一种机制,用于在内存中创建一个轻量级的文档碎片,这个文档碎片可以包含多个节点,它不涉及dom结构的实际插入,因此不会触发回流,最后带有批量节点的文档碎片插入到文档中,这样可以减少回流的次数

ini 复制代码
<script>
    const ul = document.getElementById("box");
    const fragment = document.createDocumentFragment(); // 虚拟的文档片段

    for (let i = 1; i<=100; i++) {
        let li = document.createElement("li");
        let text = document.createTextNode(i) 
        li.appendChild(text)
        fragment.appendChild(li); // 虚拟片段不会回流
    }

    ul.appendChild(fragment) 
</script>

这样只会引起一次回流

clone克隆优化

克隆指的是克隆节点,克隆过程发生在内存中,不会影响到文档布局,因此我们对副本进行操作,不引起回流,并且克隆是深拷贝,不会影响原体,克隆节点插入到文档中才会引起回流

ini 复制代码
<script>
    const ul = document.getElementById("box");
    const clone = ul.cloneNode(true) // 克隆一份ul,必定是深拷贝,原ul不受影响

    for (let i = 1; i<=100; i++) {
        let li = document.createElement("li");
        let text = document.createTextNode(i) // 创建文本节点
        li.appendChild(text)
        clone.appendChild(li);
    }

    ul.parentNode.replaceChild(clone, ul) // (替代品, 被替代品)
</script>

这样只会引起一次回流。

字节面试题

最后给大家来看一道字节面试题

ini 复制代码
<div id="app"></div>

<script>
    let el = document.getElementById('app')
    el.style.width = (el.offsetWidth + 1) + 'px' 
    el.style.width = 1 + 'px'
</script>

面试官:请问这三行js代码发生了几次回流?

第一行获取dom结构不造成回流,第二行第三行的设置宽高就会有回流。看第二行,从左往右执行,左边会进入渲染队列,但是右边会获取几何信息,因此这里按道理也会发生回流,但是这里情况特殊一点,offsetWidth**是强制触发渲染队列的执行,而非强制回流。**但是渲染队列里面没有东西去让你执行,���此执行第二行,第三行都会连续入渲染队列,最后只发生一次回流,当然,如果是老版本浏览器,就是两次回流,因为两次设置width,改变几何属性

总结

今天咱们主要来聊了聊浏览器的渲染过程,如果你能清楚的明白回流和重绘,那么浏览器后半段的过程你基本上就都掌握了,后半段整个过程就是五个阶段,生成dom树,生成cssom树,两树合并,然后回流,重绘,回流因为要计算属性,需要考虑到如何去对他进行优化,浏览器有个优化策略,就是渲染队列,只要没有计算信息的干预,会一直进行入队列,批量执行,计算信息的出现就会导致一次刷新队列,也就是回流,当然前提渲染队列是要有东西的,最后介绍了三种优化策略。如果觉得本文对你有所帮助的话可以点个免费的小赞赞嘛,感谢感谢!

相关推荐
Hello-Mr.Wang几秒前
vue3中开发引导页的方法
开发语言·前端·javascript
WG_174 分钟前
C++多态
开发语言·c++·面试
鱼跃鹰飞25 分钟前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
程序员凡尘28 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫5 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy5 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js