以下内容纯属个人的理解,如果有错误,或者讲的比较含糊的地方,欢迎大佬指正。
理解浏览器怎么解析HTML文件的,我认为有利于我们去分析一些性能问题。
ok,咱们来看看浏览器拿到HTML文件后做了什么。
拿到html文件后,首先UI线程会创建一个渲染器进程(内核),然后浏览器通过IPC通道把html数据传输给渲染器进程,接着渲染器进程的主线程把HTML字符串从上到下解析成DOM树。
HTML字符串
--->
经过tokeniser标记化,通过词法分析解析成多个标记--->
识别标记进行DOM树构造,创建Document对象,然后向下发展成完整DOM树。
大白话就是根据节点的层级关系,从第一行开始,深度优先遍历节点,将其所有节点解析成一个dom树的结构,这个dom树的节点已经是js对象了。
但是此时的dom树节点上并没有样式信息。
在dom解析的过程中,会有一些影响dom解析的因素存在,主要都是一些资源。
例如在dom中间插入script标签
<body>
<div>
1
div>
<script>
debugger
script>
<div>
2
div>
body>
你会发现,1显示在页面上了,但是此时执行了debugger代码,然后2没有显示了。这是因为阻塞了后面dom的解析。
1能正常显示出来,是不是能说明dom的解析和真正把样式渲染上去是即时的呢?还有就是有办法优化script标签阻塞问题吗?这些后面了解了整个过程后会讲。
例如图片,css文件,不会阻碍dom解析。
这个很好理解,你日常就会遇到过。例如图片很大的时候,页面已经渲染完成,但是图片一点一点的加载出来。
当head里的link链接的css资源容量很大,导致下载时间很漫长,是不会阻塞dom的解析。
但是有个误区要注意下,不阻塞dom的解析不意味着页面能正常渲染。因为页面的渲染也要等待css的解析完成才能开始,所以从最终的结果来看,是会阻塞页面的渲染。
我自己实验了一个demo,我把第一个css的link文件弄的很大,然后网络选慢速,结果页面会先白屏很久等待link文件的下载。
当然有优化的手段,后面会讲
浏览器遍历 CSS 中的每个规则集,根据 CSS 选择器创建具有父、子和兄弟关系的对象节点树。这玩意和dom树类似的,我们叫CSSOM树。
听说这个css构建到CSSOM树的速度特别快,性能不会有太大的损耗(MDN上说的)。那么我就在想了,之前网上说的那些选择器的性能问题是不是可以忽略了。
通过对DOM树和CSSOM树的遍历结合,生成的带有样式的树,被称为layout树或者render树。
这个树有一些需要注意的地方:
script
、meta
、link
等。display:none
的节点。但visibility
和opacity
隐藏的节点,还是会显示在渲染树上的,因为他们占位了。所以是不是可以证实浏览器元素审查工具上看的并不是layout树?
通过遍历layout树上每个节点的信息,确定好节点的位置,占宽(其实样式我认为也得到了,但这一步最重要的应该是确立每个节点的位置、占宽)的过程应该就叫做布局。
通过遍历layout树,把节点的层级关系确立成绘制记录表,这个过程叫做绘制。
其实这个概念有歧义,有的把内容渲染到页面上的过程也囊括在内了,例如MDN。但这里我就以不包括为主了。
所谓的层级关系就是z-index
,而绘制记录表不止记录了层级关系,还有所有的样式信息。
以上的这些事情都是主线程做的,说明主线程不仅要执行js代码,还要解析dom、做布局、做绘制。真是个卷王。
当然也会有他实在搞不过来的事情,例如接下来他要把layout树和绘制记录表给合成器线程,后者拿到后进行图层分离和栅格化。
图层分离,就是把节点安装图层关系绘制出来,例如咱们用ps软件作图,每一个图片都是用图层堆叠起来的。
那栅格化是个啥玩意?我看网上讲的好复杂,我大概理解为合成器线程把图层分离后的内容给栅格线程,栅格线程处理成一维图层,返回给合成器线程。
然后合成器线程根据拿到的东西做成一帧的内容,给GPU渲染到网页上。
如果我们此时改了节点的高度,位置,就会影响到其他节点的布局,这个时候就要重新进行布局layout以及接下来该走的流程,这就叫做重排或者回流。
如果我们只是改了个颜色、背景啥的,是不会影响到其他节点布局的,那么只需要从绘制paint开始重新走,这就是重绘。
为啥大家说尽量不要引起重排和重绘呢?因为主线程有可能在你重排重绘的时候需要执行,这个时候就会挤占浏览器的重排和重绘。
例如页面里有个方块在做往复运动,说明主线程一直在走重排、重绘、合成器线程渲染的步骤。这时候一个js程序执行了,而且还在不断的执行,此时重排、重绘的时间必定被js的执行挤占。导致重排、重绘、合成器线程渲染的步骤变慢了,那是不是每一帧的生成延迟了。
也就是说我原来1s可以渲染出60张图片的,结果因为js的执行,我只能输出30张。页面看起来是不是就卡顿了。
改善的方法也是有的!
如果要保证刷新率为60帧,那就是一帧的生成时间要为17ms内,也就是重排、重绘、合成器线程渲染的步骤要在17ms内做完。
然而每一帧的生成时间会出现可能只需要用12ms、9ms等情况,因为重拍重绘只是在主线程做,且合成器线程和栅格线程做事快滴很。
那么剩下的几ms是不是可以偷偷给js去用呢?这就是js的见缝插针。
有个叫requestAnimationFrame
的api就是专门干这个事情的,具体使用可以看这里
和
,和有
transform、opacity、filters
css属性的盒子,自己独立实例化一个图层,只在合成器线程和栅格线程执行,不会占用主线程,所以不会触发重排和重绘。
这样主线程js再怎么执行也不会影响到页面渲染,不过这样虽然可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用(MDN上说的,不是我瞎编的哈哈)。
此外还有一些方式也可以减少重排绘制:
createDocumentFragment
批量操作DOM
中,不要异步加载css文件当然一些行为会让我们没有办法保证不走重排的过程,例如窗口大小的调整也会影响页面的布局,不得已引发重排重绘。
前面提到的script标签阻塞dom解析的那个例子,我们可以看到1先展示在页面了。
是不是说明了dom解析到script标签后,会先把script标签以上的dom解析、布局、绘制、合成显示在页面上。
不过网上也有人说是dom解析、布局、绘制、合成显示这个过程是渐进式的,也就是一步一步即时运行的。
两种说法我暂时以第一种为主吧。
前面也提到说css的解析不会影响dom的解析,可并行,也就是说拿到css文件后能够和dom树一起解析。
但是到了layout那一步还是需要css的解析结果的,所以到头来还是会影响到渲染。
css解析很快,所以一般来说都是下载时间影响了页面渲染时间,要警惕link过大的问题。
我觉得就按照我写的那样理解即可(链接)。我看网上说DOMContentLoaded是dom加载完就触发,样式表,脚本等其他资源不管。我觉得不太正确,例如:
<body>
<div>1div>
<script>
window.addEventListener('load', function () {
console.log('load');
}) // 页面的全部资源加载完毕才会执行,包括图片、视频等
document.addEventListener('DOMContentLoaded', function () {
console.log('DOMContentLoaded');
}) // dom渲染完毕即可,此时图片、视频还可能没加载完。
script>
<p>2p>
<script>
debugger
script>
body>
这种情况,DOMContentLoaded都没打印出来。
具体可以看【前端面试专栏】<script> 脚本以及 <link> 标签对 DOM 解析渲染的影响,讲的还是挺清楚的。