浏览器的渲染流程

导航流程

浏览器从输入URL到页面展示间到底发生了什么?这整个流程涉及到网络请求渲染流程两大块内容。我们可以先看一下总体的导航流程。

浏览器进程构建完整的URL

  • 浏览器进程会检查输入的URL,组装协议,构建完整的的URL
  • 浏览器进程会通过进程间通信(IPC)把URL请求发送网络进程

网络进程发起URL请求

  • 检查本地缓存是有效,如果有有效,则使用本地缓存;如果无效,则进入网络请求流程
  • 网络请求的第一步:DNS解析,获取请求域名的IP地址
  • 和服务器建立TCP链接,并且构建请求信息
  • 服务器接收到请求之后,会构建响应信息
  • 浏览器接收到响应信息后,网络请求会解析响应信息, 若状态码是301/302,则会重定向到新的地址,重新发起URL请求
  • 浏览器根据响应的类型(ConTent-Type)进行处理

浏览器进程向渲染进程提交文档

  • 浏览器接收到响应信息后, 会开始准备渲染进程, 同一个站点(同根域名,同协议)会复用同一个渲染进程
  • 浏览器进程准备完毕后,浏览器进程会和渲染进程进行通信,传输文档

渲染进程开始解析页面和加载子资源,完成页面的渲染

这个过程就是渲染流程,就是我们本文重点讲的

渲染流程

构建DOM树

为什么要构建DOM树呢?因为浏览器无法识别HTML,所以需要HTML解析成浏览器识别的数据结构——DOM树。

构建CSSOM树

一样的,浏览器也无法识别CSS,所以浏览器会先将CSS解2.jpg析成浏览器能识别的数据结构—styleSheets

然后浏览器会将属性值转化成标准值,因为标准值才容易被渲染引擎理解和使用。

最后,计算DOM树˙中的每个节点的演示, 生成最终的CSSDOM树,这个过程涉及到CSS的继承规则和层叠规则。

构建渲染树(Render Tree),并计算布局

将DOM树和CSSDOM树结合就可以得到渲染树。

首先,浏览器会遍历所有可见的元素(像hea这类不可见的标签或者display设置为none的元素等会被排除在外)。

接着,找到节点所适配的的样式并应用, 最终生成渲染树。

即使现在有了渲染树,但是浏览器不知道每个节点的位置信息,所以浏览器会遍历渲染树,计算每个节点的位置信息,这就是计算布局。

分层和绘制

经过计算布局之后,并不是立马进行绘制,而是会为有3D或透视变换、z轴排序等复杂效果的节点创建图层,并生成图层树,这样做的目的是方便地实现复杂效果。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

什么情况下会创建图层呢?

  • 有层叠上下文(明确定位属性的元素, 定义透明属性的元素,使用CSS 滤镜)
  • 需要裁剪(overflow: hidden;溢出部分被剪裁)

有了图层树之后,接下来就是绘制图层了。

接着,浏览器会将图层划分为图块,这么做的目的是因为视口显示的内容有限,如果直接将整个结构进行绘制开销比较大,所以浏览器会优先将视口内的图块转为位图,这个过程叫栅格化。

最后,将位图合成,浏览器开始显示。

小结

如上图所示,HTML的渲染过程如下:

  • 将HTML解析为DOM树

  • 将CSS解析为CSSOM树

  • 将DOM树和CSSOM树构建成渲染树,并计算布局

  • 进行分层和绘制

渲染进程的特点

回流和绘制

回流和绘制是渲染进程比较重要的概念了,了解其中的概念并进行合理的的应用,可以提升性能。

回流

当元素的几何属性(尺寸),隐藏属性等改变而触发重新布局的渲染,这个过程就是回流。回流需要更新完整的渲染流程(布局-分层-绘制-图块-栅格化-合成-显示), 所以开销比较大,需要尽量避免.

触发回流的属性

  • 盒子模型相关属性(width、padding、margin、display、border等)
  • 定位属性和浮动(position、top、float等)
  • 文字结构(text-align、font、white-space、overflow等)

重绘

当元素的外观、风格等属性发生改变但不会影响布局的渲染,这个过程就是重绘。重绘省去了布局和分层阶段(绘制-图块-栅格化-合成-显示),所以性能比回流要好。回流必将引起重绘,重绘不一定会触发回流。

触发重绘的属性

color,border-style, background,outlinee,box-shadow,visibility, text-decoration

避免回流和重绘

频繁触发重绘和回流,会导致UI频繁渲染,最终导致性能变差。所以要尽量避免重绘和回流:

  • 避免使用触发重绘和回流的CSS属性
  • 将频繁重绘回流的元素创建为一个独立图层

技巧

  • 使用transform实现效果:可以避开回流和重绘,直接进入合成阶段(图块-栅格化-合成-显示)
  • 用opacity替代visibility:visibility会触发重绘
  • 使用class替代DOM频繁操作样式
  • DOM离线后修改,如果有频繁修改,可以先把DOM隐藏,修改完成后再显示
  • 不要在循环中读取DOM的属性值:offsetHeight会使回流缓冲失效
  • 尽量不要使用table布局,小改动会造成整个table重新布局
  • 动画的速度:200~500ms最佳
  • 对动画新建图层
  • 启用GPU硬件加速:启用translate3D

HTML解析的特点

顺序执行,并发加载

  • 顺序执行:HTML的词法分析事从上到下, 顺序执行
  • 并发加载:当HTML解析被脚本阻碍时,解析器虽然会停止构建DOM,但仍会识别脚本后面的内容,并进行加载
  • 并发上限:浏览器对同域名的并发数是有限制的

阻塞

css阻塞
  • css 在head中阻塞页面的渲染:避免页面闪动
  • css会阻塞jsd的执行:CSSOM构建的时候,Javascript执行将被暂停,知道CSSDOM构建完成
  • css不会阻塞外部脚本的加载
js阻塞
  • 直接引入的js会阻塞页面的渲染:当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行
  • js不阻塞资源的加载
  • js顺序执行,会阻塞后续js的执行
  • js可以查询和修改 DOM 与 CSS
改变js的阻塞

deferasync属性可以改变js的阻塞情形,不过这两个只对src方式引入的script有效,对于inline-script无效。

defer表示延迟执行,浏览器会异步地加载该脚本并且不会影响到后续DOM的渲染,该脚本将在文档完成解析后,DOMContentLoaded事件触发前执行。对动态嵌入的脚本使用 async=false 来达到类似的效果。

async表示异步执行,浏览器会异步地加载脚本并在允许的情况下执行。与 defer 的区别在于,无论是 HTML 解析阶段还是DOMContentLoaded触发之后,如果脚本加载完成,就会开始执行。需要注意的是,这种方式加载的 JavaScript 依然会阻塞load事件。

文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2018/03/24/浏览器的渲染流程/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog