浏览器如何渲染出一个完整的页面

1,822 阅读10分钟

当我们输入一个URL之后,浏览器时如何渲染出页面的?

这时,浏览器进程会通过进程间通信(IPC)把URL请求发送至网络进程,网络进程接收到URL请求之后,会在这发起真正的URL请求流程。

  1. 首先,网络进程会查找本地缓存是否缓存了该资源,如果有缓存资源,那么直接返回资源给浏览器进程,如果在缓存中没有查到资源,那么直接进入网络请求流程,这请求前的第一步是要进程DNS解析,以获取请求域名的服务器IP地址,如果请求协议是HTTPS, 那么还需要建立TLS连接。
  2. 接下来就是利用IP地址和服务器建立TCP连接,建立连接之后,浏览器端会构建请求行,请求头等信息,并把和该域名相关的Cookie等数据附加到请求头上,然后向服务器发送构建的请求信息。
  3. 服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行,响应头和响应体等信息),发送给网络进程,等网络进程接收了响应行和响应头之后,就开始解析响应头中的内容。
    • 重定向,在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是301或者302,那么说明服务器需要浏览器重定向到其他URL,这时网络进程会从响应头的Location字段里面读取重定向的地址,然后再发起新的HTTP或者HTTPS请求,一切又重头开始了。
    • 响应数据类型处理,Content-Type是HTTP头中一个非常重要的字段,它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据Content-Type的值来决定如何显示响应体的内容。

准备渲染进程

默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。

但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

提交文档

所谓提交文档,就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程,流程如下:

  1. 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起提交文档的消息。
  2. 渲染进程接收到提交文档的消息后,会和网络进程建立传输数据的管道。
  3. 等文档传输完成之后,渲染进程会返回确认提交的消息给浏览器进程。
  4. 浏览器进程在收到确认提交的消息之后,会更新浏览器界面状态,包括安全状态,地址栏的URL,前进后退的历史状态,并更新web页面。

到这里,一个完整的导航流程就走完了,之后就要进入渲染阶段。

渲染阶段

按照渲染的时间顺序,流水线分为如下几个子阶段:

  • 构建DOm树
  • 计算样式
  • 布局阶段
  • 分层
  • 绘制
  • 分块
  • 光栅化
  • 合成
构建DOM树

浏览器无法理解和使用HTML, 所以需要将HTML转换为浏览器能够理解的结构 - DOM树

domTree.awebp 现在我们已经生成了DOM树,但是DOM节点的样式我们依然不知道,要让DOM节点拥有正确的样式,这就需要计算样式了。

  1. 输入:HTML文档;
  2. 处理:HTML解析器解析;
  3. 输出:DOM数据解构;
计算样式
  1. 把css转换为浏览器能够理解的结构
  • 通过 link 引用的外部CSS文件
  • <style>标签内的样式
  • 元素的style属性内嵌的css

将上述三种css文本转换为浏览器可以理解的结构--styleSheets; 2. 转换样式表中的属性值,使其标准化

transformCSS.awebp

  1. 计算出DOM树中每个节点的具体样式 现在样式的属性已经被标准化了,接下来就需要计算DOM树中每个节点的样式属性了

利用CSS的继承规则和层叠规则进行计算标签的具体样式 1. 输入:css文本 2. 处理:属性值标准化,每个节点具体样式(继承,层叠); 3. 输出:styleSheets(CSSOM)

布局阶段

我们由DOM树和DOM树中元素的样式,但这还不足以显示页面,因为我们还不知道DOM元素的几何位置信息,那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。

Chrome 在布局阶段需要完成两个任务:创建布局树和布局计算。

  1. 创建布局树

DOM树包括很多不可见的元素,比如head标签,还有使用了display: none属性的元素,在显示之前,我们还要额外的构建一颗只包含课件元素布局树。

layoutTree.awebp 构建布局树,浏览器大体完成了下面这些工作:

  • 遍历DOM树中的所有可见节点,并把这些节点加到布局树中;
  • 而不可见的节点会被布局树忽略掉,如head标签下面的全部内容,再比如body.p.span这个元素,因为它的属性包含display:none, 所以这个元素也没有被包进布局树
  1. 布局计算

现在我们有了一颗完整的布局树,那么接下来,就要计算布局树节点的坐标位置了。并将这些信息保存在布局树中。

  1. 输入:DOM & CSSOM 合并成渲染树
  2. 处理:布局树 (DOM树中的可见元素)
  3. 输出:布局树
分层

因为页面中由很多复杂的效果,如一些复杂的3D变换,页面滚动,或者使用z-index做z轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树。

浏览器的页面实际上被分成很多图层,这些图层叠加后合成了最终的页面。

tuceng.awebp 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层

那么需要满足什么条件,渲染引擎才会为特定的节点创建新的图层呢? 通常是满足以下两点

  1. 拥有层叠上下文属性的元素会被提升为单独的一层,明确定位属性的元素,定义透明属性的元素,使用CSS滤镜的元素等,都拥有层叠上下文属性。

171ec94cc9525075_tplv-t2oaga2asx-watermark.awebp 2. 需要裁剪(clip)的地方都会被创建为图层

div {
    width: 200px;
    height: 200px;
    overflow: auto;
    background: gary;
}

我们把div的大小限定为200200元素,而div里面的文字内容比较多,文字所显示的区域肯定会超出200200的面积,这时候就产生课裁剪,渲染引擎会把裁剪文字内容的一部分用于显示在div区域

出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。

123.awebp

  1. 输入:布局树
  2. 处理:布局树中满足分层条件的元素
  3. 输出:layerTree
图层绘制

在完成图层树的绘制之后,渲染引擎会对图层树中的每个图层进行绘制

绘制操作是由渲染引擎中的合成线程来完成的

redner.awebp 当图层的绘制列表准备好之后,主线程会把该绘制列表提交给合成线程

  1. 输入:layerTree;
  2. 处理:拆分成绘制指令,生成绘制列表,提交到合成线程
  3. 输出:绘制列表
栅格化操作

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是256256或者512512

1234.awebp 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的,所谓栅格化,是指将图块转换为位图。

  1. 输入: 绘制列表;
  2. 处理: 合成线程会按照视口附近的图块开优先生成位图;
  3. 输出:位图。
合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令-DrawQuad, 然后将该命令提交给浏览器进程

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的DrawQuad 命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

  1. 输入:位图;
  2. 处理:合成线程发出命令-DrawQuad到主线程
  3. 输出:屏幕上的最终显示。
步骤总结

一个完整的渲染流程大致可总结为如下:

  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构;
  2. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets, 计算出DOM节点的样式;
  3. 创建布局树,并计算元素的布局信息;
  4. 对布局树进行分层,并生成分层树;
  5. 为每个图层生成绘制列表,并将其提交到合成线程;
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图;
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程;
  8. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上;

重排

更新元素的几何属性,例如改变元素的宽度,高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排,无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

重绘

更新元素的绘制属性,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因此并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘,相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作高一些。

合成

直接合成阶段,使用了CSS的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作,这样的效率是最高的,因为是非主线程上合成,并没有占用主线程的资源, 另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率

开发中如何减少重绘重排

  1. 使用class操作样式,而不是频繁操作style
  2. 避免使用table布局
  3. 批量dom操作,例如 createDocumentFragment, 或者使用框架 (框架使用虚拟DOM技术,最小化操作DOM
  4. window resize 这种触发概率频繁的事件要做节流