输入url到页面渲染后半段:回流,重绘,优化【一次性带你搞明白】

回顾

简单回顾一下url到页面渲染上半段:

url输入到页面渲染整个过程清楚的透彻与否,可以代表你对前端领域开发的熟悉与否

前端向后端要数据的过程:

  1. DNS解析:谁告诉你服务器在哪里
  2. 建立连接,三次握手,数据传输
  3. 断开连接,四次挥手
    http的各个版本以及每个版本解决了什么问题都是需要去认识的,以后再出期文章专门聊聊

等断开连接之后才是我们今天要聊的话题,此时浏览器已经拿到了数据

我们可以以访问百度为例,刷新百度页面的时候,检查其网络,点击全部,查看名称,可以看到所有的网络请求

这些全部都是前端向后端发送的请求,不过并不是所有的请求都是需要向服务器去要的,有些图片是缓存起来的,从本地读取,这个以后再聊。

比如我们看下第一个接口www.baidu.com

这个就是百度后端提供出来的接口,我们请求的数据就是这个样子,肯定有小伙伴就要疑惑了,数据不是一般都是json对象啥的吗,怎么是html结构。没有人规定数据只能是对象数组,百度的本地就是后端写的,这是个手段,可以解决vue首屏加载过慢问题

我们访问任何一个网站,浏览器会先拿到html代码,无外乎两种情况,一种是前后端分离式开发 :前端代码部署到了服务器上,我们访问网站时,先从前端服务器上把前端的代码加载到,该过程中会触发ajax请求,又继续向后端要数据;另一种就是前后端不分离式开发:比如jsp,把模板塞到java中,直接向浏览器输出

这时浏览器已经拿到了html代码,然后还需要拿到css代码,js代码。一般html,css,js都是分文件写的,还有图片需要加载。

好,现在我们浏览器已经拿到了html,css,js代码,网页又是如何被整成布局这么精良的样子给你看的呢,这个过程就是页面渲染

浏览器加载到了资源(html,css......)

过程如下:

html是负责结构的,css是负责样式的

  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树的意义,这样就方便绘制颜色等等

知道了两棵树还必须联合起来,属性,类名之间进行对应,这个过程非常消耗性能

dom树和cssom树结合形成的树称之为渲染树,GPU会控制物理发光点亮与否,亮什么颜色,在绘制之前会计算下布局,就像是画画上色之前,我们需要素描一下。最后才是GPU绘制整个页面

以上就是当你输入url到页面渲染的整个后过程,别看页面出现的很快,其实浏览器在背后做出了非常多的努力。

接下来,我们详细谈下第四个和第五个过程,计算布局我们称之为回流 或者重排 ,绘制页面我们称之为重绘

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

噢!不对,css是可以进行优化,这里穿插一下,我们看下下面两个写法哪个性能更高

arduino 复制代码
// 写法一
.a .b .c {

}
// 写法二
.c {

}

对于开发人员,肯定是会觉得写法一更好,更优雅,因为直接从a定位到b,再定位到c,写法二会影响所有的c这个类名的容器。但是对于生成CSSOM树而言,写法一需要先找到a容器,再找到b容器,最后才找到c容器,而写法二直接找到c容器,写法二更好。讨论这个问题之前,类名必须是唯一的。

其实这个优化微乎其微,下面我们重新认识下步骤四和步骤五,分别对应着回流和重绘,这两个过程开销的性能比较大

回流

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

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

  1. 改变窗口的尺寸
  2. 改变元素的尺寸
  3. display: none | block;(增加或删除可见元素)
  4. 页面初次渲染

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

重绘

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

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

  1. 修改背景颜色
  2. 修改背景图片
  3. 边框颜色
  4. 字体颜色
  5. 回流

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

如何减少回流,重绘

回流重绘这两个过程回流因为涉及到计算,所以它开销的性能会更多

我们看下面这个案例

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'

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

浏览器的优化策略

相比较以前的浏览器,现在的浏览器是有一个优化策略的,它执行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的宽度

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

只要是读取几何信息都会引起渲染队列的强制执行,如下方法都是:

复制代码
offsetWidth, offsetHeight, offsetTop, offsetLeft 
clientWidth, clientHeight, clientTop, clientLeft 
scrollWidth, scrollHeight, scrollTop, scrollLeft 

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

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)

执行第一个打印语句的时候,渲染队列就空了,空了的队列无法引起回流,读取值是不引起回流的,读取值引起的执行队列才会引起回流

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

那么还有什么办法减少回流重绘呢?

既然都是队列了,那么它一定有大小,所以只要队列满了就一定会执行一遍,这样才有空间去存后面的几何样式,当然这不是方法

我们可以观察页面渲染的第三步,生成Render Tree是不会带入display: none;的元素,所以我们可以先将一个元素display: none;后统一修改其样式,然后再将其display: block;回来。这也是个方法,这样就是发生两次回流,none,block分别一次。

优化实例

给你一段代码你来进行优化

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优化

现在我们用none,block方法进行优化

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>

这样就是两次回流了

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

一般字节一面大概30-45min,如果能一直聊到1h,基本上一面就稳了,30min内会问js基础问题3-5个,进阶问题(项目难点,大文件上传......),回答问题的时候最好把该知识能聊的东西全给聊一遍

最后

清楚回流重绘,那么浏览器后半段的过程你基本上就全部掌握了,后半段整个过程就是五个阶段,生成dom树,生成cssom树,两树合并,然后回流,重绘,回流因为要计算属性,需要考虑到如何去对他进行优化,浏览器有个优化策略,就是渲染队列,只要没有计算信息的干预,会一直进行入队列,批量执行,计算信息的出现就会导致一次刷新队列,也就是回流,当然前提队列是有东西的,文章最后的字节面试题就是来坑你这里的。本文还给大家介绍了三种优化策略,none,block文档碎片节点克隆

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

本次学习代码已上传至本人GitHub学习仓库:github.com/DolphinFeng...

相关推荐
阿珊和她的猫32 分钟前
v-scale-scree: 根据屏幕尺寸缩放内容
开发语言·前端·javascript
PAK向日葵3 小时前
【算法导论】PDD 0817笔试题题解
算法·面试
加班是不可能的,除非双倍日工资5 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi5 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip6 小时前
vite和webpack打包结构控制
前端·javascript
excel6 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国6 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼6 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy6 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT7 小时前
promise & async await总结
前端