通常,我们只需要编写HTML,CSS,JavaScript,浏览器上就能呈现出漂亮的网页了,但是浏览器是如何使用我们的代码在屏幕上渲染像素的呢?

首先,请先看一张大图

浏览器将HTML,CSS,JavaScript代码转换成屏幕上所能呈现的实际像素,这期间所经历的一系列步骤,叫做关键渲染路径(Critical Rendering Path)。其中包含:

  • 构建对象模型(DOM,CSSOM)
  • 构建渲染树(RenderTree)
  • 布局
  • 渲染

在构建对象模型到构建渲染树的这一过程,还穿插着JS脚本的加载和执行。如下图所示:

1.DOMTree的构建

浏览器的渲染从解析HTML文档开始,宏观上,可以分为下面几个步骤:

  • 第一步(解析):从网络或者磁盘下读取的HTML原始字节码,通过设置的charset编码,转换成相字符

  • 第二步(token化):通过词法分析器,将字符串解析成Token,Token中会标注出当前的Token是开始标签,还是结束标签,或者文本标签等。

  • 第三步(生成Nodes并构建DOM树):浏览器会根据Tokens里记录的开始标签结束标签,将Tokens之间相互串联起来_(带有结束标签的Token不会生成Node)_。
    Node包含了这个节点的所有属性。例如<img src="xxx.png" >标签最终生成出的节点对象中会保存图片地址等信息。
    事实上,在构建DOM树时,不是要等所有的Tokens都转换成Nodes后才开始,而是一边生成Token一边采取深度遍历算法消耗Token来生成Node,如下图所示:
    图中有颜色的小数字代表构建的具体步骤,可以看出,首先生成出html Token,并消耗Token创建出html 节点对象,接着生成head Token并消耗Token创建出head节点对象…,当所有的Tokens都消耗完了,紧接着DOM树也就构建完了。

这里抛出个小问题,为什么有时在js中访问DOM时浏览器会报错呢?
因为在上述的解析的过程中,如果碰到了script或者link标签,就会根据src对应的地址去加载资源,在script标签没有设置async/defer属性时,这个加载过程是下载并执行完全部的代码,此时,DOM树还没有完全创建完毕,这个时候如果js企图访问script标签后面的DOM元素,浏览器就会抛出找不到该DOM元素的错误。
值得注意的是:从bytes到Tokens的这个过程,浏览器都可以交给其他单独的线程去处理,不会堵塞浏览器的渲染线程。但是后面的部分就都在渲染线程下进行了,也就是我们常说的js单线程环境。

2.CSSOMTree的构建

DOM会记录页面的内容,但是浏览器还需要知道这些内容该用什么样式去展示,所以还需要构建CSSOMTree。CSSOM的生成过程和DOM的生成过程十分相似,也是:1.解析,2.Token化,3.生成Nodes并构建CSSOMTree:

假设浏览器收到了下面这样一段css:

body {font-size: 16px;}
p {font-weight: bold;}
p span {display:none;}
span {color: red;}
img {float: right;}

最终会生成如下的CSSOMTree:

从图中可以看出,最开始body有一个样式规则是font-size:16px,之后,在body这个样式基础上每个子节点还会添加自己单独的样式规则,比如span又添加了一个样式规则color:red。正是因为样式这种类似于继承的特性,浏览器设定了一条规则:CSSOMTree需要等到完全构建后才可以被使用,因为后面的属性可能会覆盖掉前面的设置。比如在上面的css代码基础上再添加一行代码p {font-size:12px},那么之前设置的16px将会被覆盖成12px

下面是官方给的一种解释:

未构建完的CSSOMTree是不准确的,浏览器必须等到CSSOMTree构建完毕后才能进入下一阶段。
所以,CSS的加载速度与构建CSSOMTree的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源,需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

那么回到上面生成DOM时提到的JS问题:在标签没有设置async/defer属性时,js会阻塞DOM的生成。原因是js会改变DOMTree的内容,如果不阻塞,会出现一边生成DOM内容,一边修改DOM内容的情况,无法确保最终生成的DOMTree是确定唯一的。

同理,JS也会可以修改CSS样式,影响CSSOMTree最终的结果。而我们前面提到,不完整的CSSOMTree是不可以被使用的,如果JS试图在浏览器还未完成CSSOMTree的下载和构建时去操作CSS样式,浏览器会暂停脚本的运行和DOM的构建,直至浏览器完成了CSSOM的下载和构建。也就是说,JS脚本的出现会让CSSOM的构建阻塞DOM的构建

平时谈及页面性能优化,经常会强调css文件应该放在html文档中的前面引入,js文件应该放在后面引入,这么做的原因是什么呢?

举个例子:本来,DOM构建和CSSOM构建是两个过程,井水不犯河水。假设DOM构建完成需要1s,CSSOM构建也需要1s,在DOM构建了0.2s时发现了一个link标签,此时完成这个操作需要的时间大概是1.2s,如下图所示:

而此时我们在HTML文档的中间插中入了一段JS代码,在DOM构建中间的过程中发现了这个script标签,假设这段JS代码只需要执行0.0001s,那么完成这个操作需要的时间就会变成:

那如果我们把css放到前面,js放到最后引入时,构建时间会变成:

由此可见,虽然只是插入了小小的一段只运行0.0001s的js代码,不同的引入时机也会严重影响DOMTree的构建速度。

简而言之,如果在DOM,CSSOM和JavaScript执行之间引入大量的依赖关系,可能会导致浏览器在处理渲染资源时出现大幅度延迟:

  • 当浏览器遇到一个script标签时,DOMTree的构建将被暂停,直至脚本执行完毕
  • JavaScript可以查询和修改DOMTree与CSSOMTree
  • 直至CSSOM构建完毕,JavaScript才会执行
  • 脚本在文档中的位置很重要

3.渲染树的构建

现在,我们已经拥有了完整的DOM树和CSSOM树。DOM 树上每一个节点对应着网页里每一个元素,CSSOM树上每个节点对应着网页里每个元素的样式,并且此时浏览器也可以通过 JavaScript 操作DOM/CSSOM树,动态改变它的结构。但是DOM/CSSOM树本身并不能直接用于排版和渲染,浏览器还会生成另外一棵树:Render树

接下来我们来谈几条概念

  • Render 树上的每一个节点被称为:RenderObject

  • RenderObject跟 DOM 节点几乎是一一对应的,当一个可见的 DOM 节点被添加到 DOM 树上时,内核就会为它生成对应的 RenderOject 添加到 Render 树上。

  • 其中,可见的DOM节点不包括:

  • 一些不会体现在渲染输出中的节点(<html><script><link>….),会直接被忽略掉。

  • 通过CSS隐藏的节点。例如上图中的span节点,因为有一个CSS显式规则在该节点上设置了display:none属性,那么它在生成RenderObject时会被直接忽略掉。

  • Render 树是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出,渲染引擎的输入

此时的Render树上,已经包含了网页上所有可见元素的内容和位置信息 排版引擎会根据Render树的内容和结构,准确的计算出元素该在网页上的什么位置。到此,我们已经具备进入布局的一切准备条件,但是通过上面我们知道,布局后面还有一个渲染过程,那么_Render 树是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出,渲染引擎的输入。_这句话是什么意思呢?

RenderObject and RenderLayer

浏览器渲染引擎并不是直接使用Render树进行绘制,为了方便处理Positioning,Clipping,Overflow-scroll,CSS Transfrom/Opacrity/Animation/Filter,Mask or Reflection,Z-indexing等属性,浏览器需要生成另外一棵树:Layer树

浏览器会为一些特定RenderObject生成对应的RenderLayer,其中的规则是:

  • 是否是页面的根节点 It’s the root object for the page
  • 是否有css的一些布局属性(relative absolute or a transform) It has explicit CSS position properties (relative, absolute or a transform)
  • 是否透明 It is transparent
  • 是否有溢出 Has overflow, an alpha mask or reflection
  • 是否有css滤镜 Has a CSS filter
  • 是否包含一个canvas元素使得节点拥有视图上下文 Corresponds to canvas element that has a 3D (WebGL) context or an accelerated 2D context
  • 是否包含一个video元素 Corresponds to a video element

当满足上面其中一个条件时,这个RrenderObject就会被浏览器选中生成对应的RenderLayer。至于那些没有被命运选中的RrenderObject,会从属与父节点的RenderLayer。最终,每个RrenderObject都会直接或者间接的属于一个RenderLayer。

浏览器渲染引擎在布局和渲染时会遍历整个Layer树,访问每一个RenderLayer,再遍历从属于这个RenderLayer的 RrenderObject,将每一个 RenderObject 绘制出来。可以理解为:Layer 树决定了网页绘制的层次顺序,而从属于RenderLayer 的 RrenderObject决定了这个 Layer 的内容,所有的 RenderLayerRrenderObject 一起就决定了网页在屏幕上最终呈现出来的内容。

4.布局

到目前为止,浏览器计算出了哪些节点是可见的以及它的信息和样式,接下来就需要计算这些节点在设备视口内的确切位置和大小,这个过程我们称之为“布局”。

布局最后的输出是一个“盒模型”:将所有相对测量值都转换成屏幕上的绝对像素。

5.渲染

最后,既然我们知道了哪些节点可见、它们的计算样式以及几何信息,我们终于可以将这些信息传递给最后一个阶段:将渲染树中的每个节点转换成屏幕上的实际像素:浏览器通过发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

至此,我们就能够在浏览器上看到漂亮的网页了

谈及页面性能优化,我们也常说要尽量减少浏览器的重排和重绘,浏览器重排和重绘时究竟做了哪些工作呢?

我们平时常说的重排,其实就是浏览器计算render树,布局到渲染的这个过程,而重绘就是计算layer树到渲染的这个过程,每当触发一次重绘和重排时,浏览器都需要重新经过一遍上述的计算。很显然,重排会产生比重绘更大的开销,但无论是重排还是重绘,都会给浏览器渲染线程造成很大的负担,所以,我们在实际生产中要严格注意减少重排和重绘的触发。至于如何减少重排和重绘的次数,这里就不多做展开了,详细请听下回分解~

总结:

  • 经过:1.构建对象模型(DOM,CSSOM),2.构建渲染树(RenderTree),3.布局,4.渲染 这几个步骤后,我们就能在浏览器上看到漂亮的网页啦。
  • CSS被视为阻塞渲染的资源,应放到代码的头部尽快加载。
  • 同步的JavaScript会暂停DOMTree的构建,应放到代码的尾部最后加载,或者使用async/defer属性异步加载JavaScript。
  • 重排和重绘会给浏览器渲染线程造成很大的负担,尽量减少重排和重绘的触发次数

参考文献:

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model?hl=zh-cn

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-blocking-css?hl=zh-cn

https://mp.weixin.qq.com/s?__biz=MzA5NzkwNDk3MQ==&mid=2650588806&idx=1&sn=408a54e7c8102fd6944c9a40b119015a&scene=21#wechat_redirect


版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 夜法之书浏览器的渲染过程 !
评论
数据加载中 ...
 上一篇

阅读全文

自动曝光原理
自动曝光原理 自动曝光原理
自动曝光原理简单介绍,以及其简单实现方法原理。自动曝光就是相机代替人的操作,自动调节曝光时间、光圈、ISO进行曝光,使得所摄物体亮度正常。这句话解释起来很简单,但是存在两个难点问题:第一,相机不如人眼这样可以直观的分辨图像明暗,如何判断这幅图像是否亮度合适;第二,如何调整曝光时间、光圈、ISO,这三者调节的比例。
2021-09-14
下一篇 

阅读全文

在浏览器输入 URL 回车之后发生了什么
在浏览器输入 URL 回车之后发生了什么 在浏览器输入 URL 回车之后发生了什么
在浏览器输入 URL 回车之后发生了什么? 本文将为你解答。本文的步骤是建立在,请求的是一个简单的 HTTP 请求,没有 HTTPS、HTTP2、最简单的 DNS、没有代理、并且服务器没有任何问题的基础上。
2021-09-13