浅谈浏览器加载、解析、渲染的机制

今天上午写display:none、opacity:0、visibility:hidden的时候拓展了一些关于浏览器加载,解析页面的东西,所以晚上写一篇关于浏览器是如何加载,解析,渲染的过程。其实以前也在各种论坛上看过很多大神写的关于浏览器的加载,解析,渲染的这一系列文章,但是说实话,诶呀,根本就没法统一嘛,真的是看的脑袋瓜子都大了,每个人说的都不一样...说实话给我造成了蛮大的困惑的,直到后来是看到了一篇经典的文章 howbrowserswork 对英语足够自信的朋友可以点进去看看,要是读英语觉着烦的话,也可以看看中文版的,我觉得这篇文章写得蛮好的,我自己理解的依据也大都源自于这篇文章。

几个重要的基本概念

在此之前我们需要了解一些相关的基本概念:

  • DOM(Document Object Model):浏览器将html解析为树型数据结构,简称DOM,DOM和标签基本是一一对应的关系。
  • CSSOM( CSS Object Model):浏览器将css解析为树型数据结构,简称CSSOM。
  • Redner Tree: 浏览器根据dom+cssom合并之后生成render tree。如下图:

浅谈浏览器加载、解析、渲染的机制

  • Layout:浏览器计算Render Tree中每个节点的具体位置。
  • Painting:通过显卡,将经过layout 之后的节点显示在浏览器中。
  • Reflow(回流):浏览器发现某个元素发生变化影响了布局,就会需要倒回去重新渲染。(比如执行某句js,改行js改变了某个元素的宽度,高度,那浏览器就会重新回去,对那部分元素重新渲染)

浏览器解析主体流程

上面是几个非常重要的概念,下面我们先直接给出浏览器解析的大概流程。

解析html以构建dom树 -> 构建render树 -> 布局render树 -> 绘制render树

和这个流程搭配的还有一个图,如下:

浅谈浏览器加载、解析、渲染的机制

由上述流程图我们可以发现:

  1. HTML/SVG/XHTML等会解析出一个DOM树。
  2. 解析CSS会生成一个对应的css规则树。
  3. js的脚本主要是通过DOM API 和CSSOM API来分别操作dom tree 和css rule tree

那么接下来我们看看浏览器的整个执行过程大约是怎样的:

当浏览器获得一个html文件时,会“自上而下”加载,并在加载过程中进行解析渲染。 
解析: 
1. 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。 
2. 将CSS解析成 CSS Rule Tree 。 

3. 根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。

4.有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。 
5.再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。

上述这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

上面这段内容是我看了很多大神的帖子,我觉得写得最能让人信服的一个,也是写得比较清楚的一个。

脚本js

web的模式是同步的,开发者希望解析到一个script标签时立即解析执行脚本,并阻塞文档的解析直到脚本执行完。如果脚本是外引的,则网络必须先请求到这个资源——这个过程也是同步的,会阻塞文档的解析直到资源被请求到。这个模式保持了很多年,并且在html4及html5中都特别指定了。开发者可以将脚本标识为defer,以使其不阻塞文档解析,并在文档解析结束后执行。Html5增加了标记脚本为异步的选项,以使脚本的解析执行使用另一个线程。

预解析

Webkit和Firefox都做了这个优化,当执行脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变Dom树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

样式表        

样式表采用另一种不同的模式。理论上,既然样式表不改变Dom树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,脚本可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题,这看起来是个边缘情况,但确实很常见。Firefox在存在样式表还在加载和解析时阻塞所有的脚本,而Chrome只在当脚本试图访问某些可能被未加载的样式表所影响的特定的样式属性时才阻塞这些脚本。

渲染树构建

当Dom树构建完成时,浏览器开始构建另一棵树——渲染树。渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这棵树是为了以正确的顺序绘制文档内容。

Firefox将渲染树中的元素称为frames,WebKit则用renderer或渲染对象来描述这些元素。

一个渲染对象知道怎么布局及绘制自己及它的children。

每个渲染对象用一个和该节点的css盒模型相对应的矩形区域来表示,正如css2所描述的那样,它包含诸如宽、高和位置之类的几何信息。盒模型的类型受该节点相关的display样式属性的影响

渲染树和DOM树之间的关系

渲染对象和Dom元素相对应,但这种对应关系不是一对一的,不可见的Dom元素不会被插入渲染树,例如head元素。另外,display属性为none的元素也不会在渲染树中出现(visibility属性为hidden的元素将出现在渲染树中)。

  还有一些Dom元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。例如,select元素有三个渲染对象——一个显示区域、一个下拉列表及一个按钮。同样,当文本因为宽度不够而折行时,新行将作为额外的渲染元素被添加。另一个多个渲染对象的例子是不规范的html,根据css规范,一个行内元素只能仅包含行内元素或仅包含块状元素,在存在混合内容时,将会创建匿名的块状渲染对象包裹住行内元素。

  一些渲染对象和所对应的Dom节点不在树上相同的位置,例如,浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标识出真实的结构,并用一个占位结构标识出它们原来的位置。


性能的优化

上面也说道了如果js在解析的时候发现某个元素的宽高发生变化会发生reflow,即浏览器需要回过头去重新渲染那部分区域。

那我们在知道了浏览器的渲染一个页面整体流程之后就可以在写代码时候尽量减少浏览器发生reflow的情况。

比如一张图片,我们没有设置宽高,那等浏览器请求到图片之后,发现之前img部分的尺寸肯定发生变化了,那就需要回头reflow其实避免这种情况很简单,只需要实现设定一下宽高(根据业务需求)那浏览器在第一次解析img这个标签的时候就知道要给他预留出多大的尺寸了,那等图片请求回来的时候就不会影响布局了。最多只是发生重绘,而重绘的性能消耗是要远小于重排的。

再比如我们通过js修改某个元素的样式的时候,比如通过js先修改了一个元素的高,然后在修改这个元素的宽,然后再修改这个元素的背景色,那其实我们可以把这三个需要修改的样式放在一个class中,将这个元素的class改变,样式自然就能达到我们想要的效果了,这样写不仅代码简介优雅,而且发生reflow的次数会少很多。这也是能提升浏览器性能需要注意的一个点。

好了,关于浏览器渲染机制就写到这儿,如果各位大神有什么不同的简介,非常欢迎一起交流。如有错误,也欢迎指正。