【计算机原理交集】看了无数篇文章后,我自己梳理了下浏览器是怎么解析HTML文件的

前言

理解浏览器怎么解析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树

这个过程叫做布局

这个树有一些需要注意的地方:

  1. 一些不会挂在Layout树的标签,比如scriptmetalink(因为浏览器默认样式给他们设置了display:none)等。
  2. Render树上虽然没有属性为display:none的节点。但visibilityopacity隐藏的节点,还是会显示在渲染树上的,因为他们占位了。
  3. 伪元素不会显示在DOM树,但会显示在渲染树上。
  4. 还有匿名行盒、匿名会盒也是在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、filterscss属性的盒子,自己独立实例化一个图层,只在合成器线程执行,不会占用主线程,所以不会触发重排和重绘。

这样主线程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

唉呀妈呀!太辛苦了,求赞~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值