回顾
简单回顾一下url到页面渲染上半段:
url输入到页面渲染整个过程清楚的透彻与否,可以代表你对前端领域开发的熟悉与否
前端向后端要数据的过程:
- DNS解析:谁告诉你服务器在哪里
- 建立连接,三次握手,数据传输
- 断开连接,四次挥手
http的各个版本以及每个版本解决了什么问题都是需要去认识的,以后再出期文章专门聊聊
等断开连接之后才是我们今天要聊的话题,此时浏览器已经拿到了数据
我们可以以访问百度为例,刷新百度页面的时候,检查其网络,点击全部,查看名称,可以看到所有的网络请求
这些全部都是前端向后端发送的请求,不过并不是所有的请求都是需要向服务器去要的,有些图片是缓存起来的,从本地读取,这个以后再聊。
比如我们看下第一个接口www.baidu.com
这个就是百度后端提供出来的接口,我们请求的数据就是这个样子,肯定有小伙伴就要疑惑了,数据不是一般都是json对象啥的吗,怎么是html结构。没有人规定数据只能是对象数组,百度的本地就是后端写的,这是个手段,可以解决vue首屏加载过慢问题
我们访问任何一个网站,浏览器会先拿到html代码,无外乎两种情况,一种是前后端分离式开发 :前端代码部署到了服务器上,我们访问网站时,先从前端服务器上把前端的代码加载到,该过程中会触发ajax请求,又继续向后端要数据;另一种就是前后端不分离式开发:比如jsp,把模板塞到java中,直接向浏览器输出
这时浏览器已经拿到了html代码,然后还需要拿到css代码,js代码。一般html,css,js都是分文件写的,还有图片需要加载。
好,现在我们浏览器已经拿到了html,css,js代码,网页又是如何被整成布局这么精良的样子给你看的呢,这个过程就是页面渲染
浏览器加载到了资源(html,css......)
过程如下:
html是负责结构的,css是负责样式的
- 解析html代码,生成一个dom树
- 解析css代码,生成CSSOM树
- 将dom树和CSSOM树结合,
去除不可见的元素
,生成Render Tree
- 计算布局,(
回流
|重排
)根据Render Tree
进行布局计算,得到每一个节点的几何信息 - 绘制页面,(
重绘
)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容器,写法二更好。讨论这个问题之前,类名必须是唯一的。
其实这个优化微乎其微,下面我们重新认识下步骤四和步骤五,分别对应着回流和重绘,这两个过程开销的性能比较大
回流
浏览器计算页面布局的过程就叫做回流
只要页面有容器几何信息发生变更就会发生回流,也就是影响了它的排版,所以回流太常见了,有以下几种:
- 改变窗口的尺寸
- 改变元素的尺寸
- display: none | block;(增加或删除可见元素)
- 页面初次渲染
容器脱离文档流是不会发生回流的,当然这是针对影响其他元素而言,比如你增删一个可见元素,是会影响下面容器的几何信息的。对于脱离文档流容器本身而言,肯定是发生回流的
重绘
GPU将已经计算好几何信息的容器在屏幕上亮起来就是重绘
所以只要元素的非几何属性发生变化时,就会发生重绘
- 修改背景颜色
- 修改背景图片
- 边框颜色
- 字体颜色
- 回流
注意,既然发生了回流,就一定会带来重绘,重绘不一定带来回流
如何减少回流,重绘
回流重绘这两个过程回流因为涉及到计算,所以它开销的性能会更多
我们看下面这个案例
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...