前端基础知识——浏览器渲染一个页面的全过程总结

前言:

如果你在研究“浏览器如何渲染页面”这个老生常谈的话题,并且看了一部分相关文章,那么不妨花5分钟看一下我的理解,或许对你有所帮助。

(最近事情有点多,乱糟糟的,学习也浮躁,文章是我好一段时间前写的了,本还想基于理论再整点优化的实践,也没落实,不过可以放心下面文字的质量我是有信心的)

关键渲染路径(一个概念)

定义:就是我们的浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。

个人理解:这个过程中包含文档对象模型(DOM)的构建,CSS对象模型(CSSOM)的构建,渲染树的构建以及布局、绘制等具体的步骤。感觉关键渲染路径就是在描述页面首屏渲染的流程,所以优化关键渲染路径可以提高渲染性能(首屏渲染优化)。

浏览器渲染流程概图

浏览器渲染流程图.png

一、HTML解析细节(& 思考)

浏览器渲染首先就是解析HTML文件,以构建DOM(document object module)树以及CSSOM树

这里需要一点关于浏览器线程与进程的基础知识:浏览器是多进程模型,其中每个tab页都有一个渲染进程,渲染进程里的常驻线程有:GUI渲染线程,负责解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等都是这个线程去做的;还有就是JS引擎线程,也就是常说的主线程,负责js代码的执行,GUI渲染线程与JS引擎线程是互斥的

  1. Webkit 和 Firefox浏览器都进行了预解析优化,也就是说在GUI渲染线程去解析HTML之前,可以理解为开启一个预解析行为,具体就是扫描html文件中外链的js脚本(<script src=xxx>)和css样式文件(<link href=xxx>),然后开启网络请求线程去请求这些资源,相当于提前准备这些资源,以便在GUI渲染线程解析到js脚本以及css文件时可以直接使用。

  2. 然后就是GUI线程在解析HTML过程中遇到<link>的外链css时如何处理,首先css文件加载完成与否并不会影响GUI线程的继续解析,也就是说如果css文件还没准备好,那么GUI就继续向下解析html来构建DOM树,等待css文件准备好了,那么GUI渲染线程可能就会停止DOM的构建然后来处理CSSOM的构建(由上面基础知识我们知道DOM与CSSOM的构建都是由GUI渲染线程去做的)

    这里补充一个浏览器处理的具体细节:GUI渲染线程处理到<link>时就会触发一次渲染,当<link>里的样式解析完毕,生成CSSOM进而合成render tree、布局等等操作之后还会进行一次渲染。

    这也就可以解释为什么 “css要放在上面” ,因为css放在上面,比如放在<head>里面,根据上面的理论,GUI线程解析html文档时第一次遇到外链css进行渲染尝试,因为此时压根没有DOM(需要渲染的dom结构),自然也没有render tree,所以就不会渲染,等到这个外链css加载并且解析完毕了之后,GUI渲染线程再次进行渲染,这样就保证了页面首次渲染即为有样式的。对比css放在下面,GUI线程遇到<link>时DOM树已经存在了,但是此时还没有CSSOM树,所以导致render tree没有样式,所以就会造成初次渲染的无样式,当css文件处理完毕,GUI线程进行第二次渲染,必然发生重绘甚至回流(重排),并且样式从有到无出现扇动,对用户体验来说应该也是不好的。理论的实践依据可参考这篇文章对于解析css浏览器行为的探索

  3. GUI线程解析HTML过程中如果遇到<script>,把握住一个原则:GUI渲染线程与JS引擎线程是互斥的,所以js的执行一定会阻塞GUI渲染线程对html的继续解析,也就是说GUI渲染线程停止执行,切换为js引擎线程来工作。

    我们可以思考一下为什么要设计GUI渲染线程与js引擎线程互斥,其实非常好理解,因为js完全有能力去影响DOM以及CSSOM,所以一旦js执行,GUI线程对于DOM和CSSOM的构建就该停止了,不然两者就会发生冲突。

    上面提到,js不止有能力影响DOM,也有能力影响CSSOM(js修改样式基本操作罢了),所以还有一个HTML解析的具体细节就是:<link>后面有js代码时,那么只有<link>下载并解析完毕,js代码才能执行(保证js正确访问css)。所以从这个角度来说, <link>也阻塞了后续DOM的解析(<link>阻塞后面的<script><script>会阻塞dom解析)。

    所以这里我们也可以分析一下为什么js要放在下面了,非常简单直接,就是因为不想让js的执行阻塞GUI渲染线程的工作。

  4. 特殊情况:上面所说的<script>都是最普通的情景,<script>标签有两个属性deferasync,HTML解析时的具体特点如下图,简单概括一下就是拥有defer或者async属性的<script>的下载都不会阻塞HTML的解析,defer的js内容会在HTML解析完毕后执行,async的js内容会在<script>下载准备完毕之后立即抢占GUI渲染线程来执行,也就是说啥时候执行成为了一个未知数(全看网络状况)

    总而言之,为了让<script>不影响HTML的解析,我们一般都会给<script>添加defer属性(去看一下vue或者react的打包产物,都是defer<script>),这样<script>的位置也就不那么重要了。

    多说一句,这里牵扯一个DOMContentLoaded事件执行时机的问题,添加了defer属性的<script>脚本执行完毕之后才会触发DCL事件,换句话说,DCL事件触发之前执行defer脚本。

async与defer.png

二、构建render tree

通过上面的HTML解析,我们拿到了DOM与CSSOM,并将其合称为render tree,说白了就是给DOM树的每个节点添加上样式属性,当然这个过程中display: none的元素会被删除,不会出现在render tree中。合成过程中还会将一些css属性进行转换,比如:red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成`px。

三、布局

就是根据render tree各节点的一些位置属性生成布局树,说白了就是明确这些元素将来渲染的一个位置。

四、分层

也就是我们的元素布局可能并不是在一个平面上,而是分了若干个图层进行渲染。

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。

如下简单举几个产生新图层的例子:

  1. 普通元素设置position不为static并且设置了z-index属性,会产生层叠上下文。
  2. 元素的 opacity 值不是 1
  3. 元素的 transform 值不是 none

五、绘制

GUI渲染线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。

这里注意GUI线程到这里就要让位了:完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

绘制之后还有分块(合成线程首先对每个图层进行分块,将其划分为更多的小区域)、光栅化(光栅化的结果,就是一块一块的位图)操作,但是感觉不是很重要,然后就是最后一步了:画(真正在浏览器上进行渲染展示)

六、画

合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。

变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。

合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件(硬件加速),完成最终的屏幕成像。

什么是reflow(回流/重排)?

reflow 的本质就是重新计算 layout 树(根据render树生成的布局树)。

当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。

为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 reflow 是异步完成的

也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。

浏览器在反复权衡下,最终决定获取(元素位置)属性立即 reflow。

什么是 repaint(重绘)?

repaint 的本质就是上面的第五步,即重新根据分层信息计算绘制指令。

当改动了可见样式后,就需要重新计算,会引发 repaint。

render树之后就是计算layout布局树,步骤在paint绘制之前,所以 reflow 一定会引起 repaint。

为什么 transform 的效率高?

因为 transform 既不会影响布局也不会影响绘制指令,它影响的只是渲染流程的最后一个「draw」阶段 (处于绘制的最后一个阶段,不会牵动前面的步骤执行)

由于 draw 阶段在合成线程中,所以 transform 的变化几乎不会影响渲染主线程。反之,渲染线程无论如何忙碌,也不会影响 transform 的变化。 (transform所属的线程为合成线程,不抢占渲染线程以及js引擎线程)

全部评论

相关推荐

牛客928043833号:在他心里你已经是他的员工了
点赞 评论 收藏
分享
评论
2
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务