带你走近浏览器的渲染流水线

1,511 阅读8分钟

导航流程

浏览器从输入URL到页面展示间到底发生了什么?这是一个非常经典的前端面试题目了,这个题目非常考验前端功底,整个流程涉及到网络请求和渲染流程两大块内容,那我们先回顾下导航流程吧。

1、浏览器进程构建完整的URL

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

2、网络进程发起URL请求

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

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

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

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

这个过程就是渲染流程了,下面我们具体地了解下这个流程。

渲染流程

1、构建DOM树

为什么要构建DOM树?因为浏览器无法识别HTML,所以要把HTML解析成浏览器能识别的数据结构——DOM树。下图所示是一个DOM树。

2、构建CSSOM树

同样地,浏览器也无法识别CSS,所以浏览器会先将CSS解析成浏览器能识别的数据结构——styleSheets。

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

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

3、构建布局树

现在,我们拥有了DOM和CSSOM这两个独立的对象,浏览器将这两个对象模型结合,构建布局树。

首先,浏览器会遍历所有可见的元素(像head这类不可见的标签或者dispaly设置为none的元素等会被排除在布局树外),接着,找到节点所适配的样式并应用。

此时浏览器还不知道每个节点的位置信息,所以浏览器会遍历布局树,计算每个节点的位置信息,这就是计算布局。

4、分层和绘制

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

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

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

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

浏览器会把一个图层拆分为一个个小的绘制指令,然后将指令按照顺序排成一个列表。绘制过程和使用canvas进行绘制图类似。

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

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

小结

从输入URL到页面呈现的整体流程:

  • 浏览器进程构建完整URL,并通过进程间通信将URL提交给网络进程
  • 网络进程:
    • 检查缓存
    • DNS解析
    • 建立TCP连接(三次握手)
    • 发送请求数据
    • 接收响应数据,并根据响应数据类型进行解析
  • 网络进程将处理好的数据提交给浏览器进程,浏览器进程准备好渲染进程
  • 渲染进程:
    • 将HTML解析为DOM树
    • 将CSS解析为CSSOM树
    • 将DOM树和CSSOM树构建成布局树
    • 进行分层和绘制

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

渲染流程的特点

回流和重绘

这两个是渲染过程中比较重要的概念了,了解其概念并进行合理应用,可以提升性能。

回流

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

触发回流的属性

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

重绘

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

触发重绘的属性

color、border-style、background、outline、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,但仍会识别该脚本后面的资源,并进行预加载。
  • 并发上限:浏览器对同域名的并发数是有限制的(HTTP/2则没有这个限制)

阻塞

css阻塞

  • css在head中阻塞页面的渲染:避免页面闪动
  • css会阻塞js的执行:CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。
  • css不阻塞外部脚本的加载

默认情况下,CSS会阻塞渲染,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。不过,使用媒体查询可以让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事件。

DOMContentLoaded

MDN的解释:当初始的 HTML 文档被完全加载和解析完成之后,
DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。

load

MDN的解释:load 应该仅用于检测一个完全加载的页面
当一个资源及其依赖资源已完成加载时,将触发load事件。