文章目录
前言
理解浏览器怎么解析HTML文件的,我认为有利于我们去分析一些性能问题,并且能对浏览器的工作有更加深入的了解。
ok,咱们开始来看看浏览器拿到HTML文件后到底做了什么。
因为网上关于这块的讲解各种各样,毫不统一,所以以下内容纯属个人的理解,如果有错误,或者讲的比较含糊的地方,欢迎大佬指正。
HTML字符解析
主要分为DOM的解析与CSS的解析
DOM的解析
网络线程拿到html文件后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。
在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。
主线程把HTML字符串从上到下解析成DOM树。
大白话就是根据节点的层级关系,创建Document对象,从第一行开始,深度优先遍历节点,将其所有节点向下,一边解析一边创建js的dom对象,最终成一个dom树的结构。
但是此时的dom树节点上并没有样式信息。
CSS的解析
主线程会根据每一个样式表(link文件、style标签写的样式、dom上的内联样式,浏览器默认样式),去往下发展成一个树结构。
大致是这样的,例如一个link文件,下一级就是所有的css选择规则,然后每个规则节点下面有两个节点,第一个节点表示选择规则的具体信息,第二个表示style,style下面的节点就是具体的css属性信息。
这个就叫做CSSOM树,css object model树。
听说这个css构建到CSSOM树的速度特别快,性能不会有太大的损耗(MDN上说的)。那么我就在想了,之前网上说的那些选择器的性能问题是不是可以忽略不记了。
用document.styleSheets
可以拿到CSSOM,可以用js操作,例如给第一个样式表所有的div加上边框
document.styleSheets[0].addRule('div', 'border: 2px solid #f40 !important')
转成DOM树和CSSOM树是为了浏览器能更好的对他们解析和做后续工作(纯字符串是无法完成后期渲染工作的),并且能让js去控制。
知识拓展,css的计算过程主要是:
- 确定好css文件里的样式
- 通过权重计算出每个属性的最终值
- 通过继承计算出还没有值的属性
- 通过浏览器的默认样式把剩余没有值的属性添加上值
影响HTML字符解析的资源
在解析的过程中,会有一些影响解析的因素存在,主要都是一些资源。例如
script标签
在dom中间插入script标签
<body>
<div>
1
</div>
<script>
debugger
</script>
<div>
2
</div>
</body>
你会发现,1显示在页面上了,但是此时执行了debugger代码,然后2没有显示了。这是因为阻塞了后面html的解析。
1能正常显示出来,是不是能说明dom的解析和真正把样式渲染上去是即时的呢?还有就是有办法优化script标签阻塞问题吗?这些后面了解了整个过程后会讲。
如果script标签是外联的文件那更惨,一定会优先下载完js文件后,执行完,再继续解析。这是因为有可能执行的这个js文件中需要修改前面解析好的dom树,所以后面的dom解析必须暂停先。
不会影响HTML字符解析的资源
具体是这样的,在主线程开始做解析的时候,会启动一个预解析线程,它会快速的过一遍html,发现有一些资源请求的标签,会优先处理,例如link标签,为了不给主线程造成阻塞,会让网络线程去下载对应的文件,下载并且自己解析完成后再交给主线程。
CSSOM树的生成还是主线程做的,预解析线程不会生成,只是解析。
还有图片的下载也不会影响HTML的解析。
图片
这个很好理解,你日常就会遇到过。例如图片很大的时候,页面已经渲染完成,但是图片一点一点的加载出来。
link标签细讲
但是有个误区要注意下,css的下载解析不阻塞html的解析不意味着页面能正常渲染。因为页面的渲染也要等待所有css的解析完成才能开始。
所以当head里的link链接的css资源容量很大,导致下载时间很漫长,从最终的结果来看,是会阻塞页面的渲染。
我自己实验了一个demo,我把第一个css的link文件弄的很大,然后网络选慢速,结果页面会先白屏很久等待link文件的下载。当然有优化的手段,后面会讲。
样式计算
主线程通过遍历DOM树和CSSOM树,给每个节点算出最终的样式属性。
在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
最终计算样式可以这样获取getComputedStyle()
然后这颗DOM树就带有样式了。
布局Layout
通过遍历DOM树上每个节点的信息,确定好节点的相对包含块的位置,占宽等),生成Layout树或者Render树。
这个过程叫做布局。
这个树有一些需要注意的地方:
- 一些不会挂在Layout树的标签,比如
script
、meta
、link
(因为浏览器默认样式给他们设置了display:none
)等。 - Render树上虽然没有属性为
display:none
的节点。但visibility
和opacity
隐藏的节点,还是会显示在渲染树上的,因为他们占位了。 - 伪元素不会显示在DOM树,但会显示在渲染树上。
- 还有匿名行盒、匿名会盒也是在Layout树上会有(了解即可)
所以可以证实浏览器元素审查工具上看的并不是Layout树
为什么这些位置信息在样式计算的阶段没有计算出来?因为不是所有的宽高都能算出来的,例如百分比,auto之类的。
Layout树已经不是DOM节点了,而是c++节点。
拿Layout树的宽高可以这样拿:document.body.clientWidth
分层Layer
上述搞完后,主线程会对布局树要展示的内容做分层。例如咱们用ps软件作图,每一个整体的图片都是用图层堆叠起来的。所以浏览器根据他认为最好的方式,把一些大的树分支单独分层。这样的好处是以后这个图层的内容发生变化,只对这层做后续的处理即可。
Layer可以在浏览器控制台调出,看页面具体怎么分层的。
像一些属性z-index,transform、opacity
等,不是一定就能影响分层结果的。
如果想直接影响分层结果,用will-change: width;
属性,告知浏览器这个盒子的width属性会经常变动,建议独自分一层。
不过分层不要滥用,过多反而变成了负优化。
绘制Paint
主线程会为每个分层单独产生绘制指令集(一条条绘制指令的集合),用于描述这一层的内容该如何画出来。
接着主线程把每个图层的指令集信息交给合成线程,后者会先对每个画出来的图层进行分块(Tiling),切割成更小的区域。
分块的目的就是为了优先显示视口内的内容。
其实这个概念有歧义,有的把内容渲染到页面上的过程也囊括在内了,例如MDN。但这里我就以不包括为主了。
光栅化
所以光栅化就理解为,把每个块处理成像素图(位图)。这个处理过程是合成线程交给GPU进程做的,多个线程进行处理,并且优先处理视口内的快。做完后交还给合成线程。
画
合成线程拿回数据后,生成一个个指引(quad),指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。
变形发生在合成线程,与渲染主线程无关,这就是transform
效率高的本质原因。
合成线程会把指引提交给GPU进程,由GPU进程产生系统调用,提交给GPU硬件,完成最终的屏幕一帧的成像。
重排(回流)和重绘
如果我们此时改了节点的高度,位置,就会影响到其他节点的布局,这个时候就要重新进行布局以及接下来该走的流程,这就叫做重排或者回流,英文叫reflow。
如果我们只是改了个颜色、背景啥的,是不会影响到其他节点布局的,那么只需要从绘制paint开始重新走,这就是重绘,英文叫repaint。
为啥大家说尽量不要引起重排和重绘呢?
例如页面里有个方块在做往复运动,说明主线程一直在走重排、重绘、合成器线程渲染的步骤。这时候一个js程序执行了,而且还在不断的执行,此时重排、重绘的时间必定被js的执行挤占。导致重排、重绘、合成器线程渲染的步骤变慢了,那是不是每一帧的生成延迟了。
也就是说我原来1s可以渲染出60张图片的,结果因为js的执行,我只能输出30张。页面看起来是不是就卡顿了。
改善的方法也是有的!
浏览器会自行优化
例如以下代码:
box.style.width = '100px'
box.style.margin = '20px'
浏览器会合并后异步执行。
但此时如果你在最后一行获取某个属性值的时候,浏览器为了能够让你获取到最新值,不得不同步去执行了:
box.style.width = '100px'
box.style.margin = '20px'
log(box.clientWidth)
js见缝插针的执行
如果要保证刷新率为60帧,那就是一帧的生成时间要为16.6ms内,也就是重排、重绘、合成器线程渲染的步骤要在16.6ms内做完。
然而每一帧的生成时间会出现可能只需要用12ms、9ms等情况,因为重排重绘只是在主线程做,且合成器线程做事快滴很。
那么剩下的几ms是不是可以偷偷给js去用呢?这就是js的见缝插针。
有个叫requestAnimationFrame
的api就是专门干这个事情的,具体使用可以看这里
不走重排和重绘
可控的地方
<video>
和 <canvas>
,和有transform、opacity、filters
css属性的盒子,自己独立实例化一个图层,只在合成器线程执行,不会占用主线程,所以不会触发重排和重绘。
这样主线程js再怎么执行也不会影响到页面渲染,不过这样虽然可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用(MDN上说的,不是我瞎编的哈哈)。
此外还有一些方式也可以减少重排绘制:
- 避免多次随意修改样式,可以一次同时修改
- 利用BFC
- 使用
createDocumentFragment
批量操作DOM - css放在
<head>
中,不要异步加载css文件 - img标签提前定义宽高,例如在做图片懒加载的时候
这里单独说个transform,既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个画的阶段。
同样的滚动动作只会走画这个阶段,所以主线程卡死不影响页面滚动。
canvas直接就可以走绘制的流程。
不可控制的地方
当然一些行为会让我们没有办法保证不走重排的过程,例如窗口大小的调整也会影响页面的布局,不得已引发重排重绘。
合成
网上有习惯把从合成线程开始做做的事情统称为合成。
也就是说更新是一个这样的过程:重拍-重绘-合成。
不过这种自定义概念的东西看个人习惯吧。
为什么前面例子中1先显示了?
前面提到的script标签阻塞dom解析的那个例子,我们可以看到1先展示在页面了。
其实我认为是这样的,当用了debugger时,就会以现有的解析情况走完后续所有流程。
CSS会阻塞HTML的解析吗
前面提到了不再赘述了,优化方法在后面的link标签使用
window.onload和DOMContentLoaded的区别
我觉得就按照我写的那样理解即可(链接)。我看网上说DOMContentLoaded是dom加载完就触发,样式表,脚本等其他资源不管。我觉得不太正确,例如:
<body>
<div>1</div>
<script>
window.addEventListener('load', function () {
console.log('load');
}) // 页面的全部资源加载完毕才会执行,包括图片、视频等
document.addEventListener('DOMContentLoaded', function () {
console.log('DOMContentLoaded');
}) // dom渲染完毕即可,此时图片、视频还可能没加载完。
</script>
<p>2</p>
<script>
debugger
</script>
</body>
这种情况,DOMContentLoaded都没打印出来。
怎么修改link标签和script标签对页面加载的影响
具体可以看【前端面试专栏】<script> 脚本以及 <link> 标签对 DOM 解析渲染的影响,讲的还是挺清楚的。
怎么模拟JS主线程阻塞
这里给大家一个函数:
function delay(duration = 1000) {
let startDate = Date.now()
while (Date.now() - startDate < duration) { }
}
delay(3000) // 阻塞3s
唉呀妈呀!太辛苦了,求赞~