浏览器渲染器 工作机理

86 阅读9分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第N天,点击查看活动详情

浏览器渲染原理

引言

第一个问题,什么是浏览器的渲染?

作为长期的电子设备使用者,我们已经习惯了各种页面的流畅显示,已经理所当然,觉得本就该那样。

但作为一名工程师,我们不应该以这样的角度观察我们的计算机,我们应该产生一个疑问--

我的电脑是怎么确定显示器上每个像素点的颜色的?

所谓的浏览器页面渲染,就是指浏览器将得到的html字符串经过一系列的处理加工,获取信息,最终告诉我们的显卡每个像素点的信息,从而显示出我们看到的网页。

提示: 理解本篇内容,需要对渲染主线程的事件循环机制有所了解,可以查阅资料或者看看我写的这篇文章: 【深入浏览器】事件循环机制详解

前奏

  1. 打开一个Tag(标签),浏览器启动一个渲染进程,渲染主进程开始执行
  2. 输入url地址按下回车,网络线程获取资源,拿到 html 资源
  3. 网络线程将 html 资源包装成渲染任务放入消息队列
  4. 渲染主线程通过事件循环机制拿到了渲染任务,开始执行渲染任务。

image.png

渲染任务

1. Parse HTML

渲染任务的第一步是 解析 html, 这一步的作用是,将 html 文本信息转换成后续方便操作的结构,即 dom 树 和 cssom 树。

并且,JS代码也在这个环节执行。

渲染主线程开始执行渲染任务,对获取到的html进行解析

在这个阶段,渲染主线程会根据信息将html文本转化为 DOM TreeCSSOM Tree

image.png

DOM Tree

渲染主线程接到解析html的任务,开始着手解析

渲染主线程先创建 document 节点,接着扫描 html 文件,形成dom树

dom树的本质是引擎利用 html 信息生成方便后续操作的对象,js操作的dom对象是在此基础上的包装

image.png

dom树的特点是:

  • document节点是根节点
  • 每个节点只有一个父节点

CSSOM Tree

image.png

根节点 StyleSheetList:代表网页中所有的样式表

一个容易忽视掉的细节是:

document 对象有一个 styleSheets 属性。 该属性其实就是当前页面的除浏览器默认样式外cssom tree,我们修改它也可以实现修改 css (这里是直接修改cssom tree, 而不是通过内联样式)

当html解析时,遇到 style 标签,会进行 CSSOM 生成,同时,执行任务队列的 CSS 任务也会生成 CSSOM(后面会讲)

CSSstyleSheet的每个子节点都是一组 CSSstyleRule , 该节点包含两个子节点,分别是 选择器style

我们其实可以通过给doucument的CSSstyleSheet属性添加 CSSstyleRule实现修改样式:

document.styleSheets[0].addRule('div',border:2px solid #f40 !improtant);

页面所有 div 都添加了红色边框

CSS解析会堵塞dom解析吗?

生成 dom tree 和生成 cssom tree 之间是不会产生影响的,在此前提下,为了提高效率,浏览器会启动预解析线程快速浏览html,遇到 link 标签,则通知 网络线程 开始下载CSS外部资源,网络线程将下载好的资源交给预解析线程进行解析,预解析线程解析css完毕后,会将结果包装成任务放入任务队列,主线程拿到预解析后的结果可以快速生成cssom,从而提升效率。

image.png

可以看到,css下载和html解析是运行在两个线程上的,所以css下载不会堵塞html解析

反思

为什么要将html和css解析成DOM和CSSOM ?

因为页面的后续工作可能经常要对html结构和css进行操作,而字符串类型读取信息和操作都比较麻烦,尤其是很难获取一些上下文关系

因此,将html和css转换成对象进行读取和操作是一种自然而然的想法。

而且,也正是这种方式才为js提供了操作 html 和 css的能力。(DOMAPI的实现)

解析到script标签

预解析线程也会对script标签外链的文件进行预下载

但与CSS不同的是,当html遇到script标签时,会停止html解析(GUI线程挂起)

必须等待该Script脚本执行完毕才能继续解析html

为什么?

因为js可能会操作已经生成的dom,所以必须让js先执行才能继续解析dom。

2.Recalculate Style

解析完整个html后,将 dom tree 和cssom tree 的信息整合,计算出每个节点的最终样式

你可能不知道的细节:

其实在这个过程中,会为每个节点生成一套完整的属性和值,换句话说,其实每个dom元素都拥有所有css属性

关于这个过程的详细原理

请看我的这篇文章 你绝对不知道的css底层原理

image.png

该过程后,每个节点都有了确定的样式属性

3. LayOut

该阶段是布局

根据每个节点的样式计算出每个节点的尺寸,位置

image.png

你可能不知道的细节:

这个步骤中所谓的位置是相对于包含块的位置,如果你还不清楚包含块的概念,请看我写的这篇文章:

你不知道的CSS-包含块

LayOut 树和DOM树的结构不同的原因:

你可能观察上图注意到了,DOM树和布局树的结构似乎不相同,这是为什么呢?

原因1:几何信息

因为布局树需要得到每个元素的几何信息,而有些节点并没有几何信息,比如display none的元素之所以不会显示在最终的页面上,也不会影响布局的原因,是布局树会忽略掉这些元素,比如 head 标签 的默认样式就是display: none

并且一些伪元素比如 ::after 因为有几何信息会出现在 layout 树中,所以伪元素影响布局

所以,dom树中虽然没有伪元素,但是布局树中有。

原因2:块盒和行盒的规则

布局的规则有以下两个原则:

  • 内容必须在行盒中
  • 行盒和块盒不能相邻

image.png

p节点不是行盒(浏览器默认样式), 而由于内容 "a" 需要在行盒中,所以在a节点的父节点新建了一个匿名行盒

b节点应该在匿名行盒中,但行盒不能和块盒相邻,因此会创建一个匿名块盒(这里图画错了)

JS是没有直接访问布局树的能力的,但底层为document对象暴露一些属性用于获取布局信息,比如 document.body.clientWidth

4. layers

该步骤是分层

image.png

分层的目的是为了提高后期操作页面的效率,只需要修改发生变化的层次,而不用修改整个页面

5. Paint

第五步是绘图

这里的绘图不是已经开始画像素点了,而是生成绘图指令,比如在哪那个坐标画一个什么样的图像,用什么颜色填充 (canvas与之类似的工作原理)

到此,渲染主线程的工作到此为止,渲染工作的其他部分交给浏览器的其他线程完成。

6. Tiling

第六步是分块

第五步结束后,渲染主线程将绘图指令信息交给合成线程。合成线程完成第六步分块任务。 合成线程为了提高效率,又会开启多个子线程进行分块。

image.png

分块是将每个层(第四步是分层)分为一些小块。分块的目的是,一般整个页面不会完全展示在显示器上,因此,将靠近视口的块优先绘制。

image.png

7.光栅化

第八步是光栅化,生成像素点信息,到了这一步,浏览器终于可以根据数据将文本转为像素信息

在这一步里,会用到GPU加速,合成线程光栅化的工作交给GPU进程,因为GPU进程效率更高,当GPU完成光栅化任务后,将位信息重新交给合成线程,合成线程利用信息形成位图。

每个块转化成位图,优先处理靠近视口的块

image.png

8. draw

我们已经有了每个像素点的信息,终于可以在显示器上显示图像了。

第八步就是这个作用。

image.png

这一步又叫做draw quad,quad是辅助显卡确定像素点在显示器的具体位置的信息

合成线程利用GPU进程作为中转,将位图信息交给显卡硬件,最终显卡具体的像素生成在显示器上,我们看到图像。

为什么合成线程不直接把位图信息交给硬件而是需要GPU进程中转呢?

这是因为合成线程和渲染主线程在渲染进程中执行的, 而渲染进程是在一个沙盒中,与外界是隔离的,目的是保证计算机的安全性,不至于网页调用硬件损坏电脑。

值得一提的是,css3中transform属性动画,是在第八步确定像素的,因此效率比较高。

总结

渲染过程实际上是一个流水线,每个阶段的输出是下一个阶段的输入,html字符串经过浏览器各种线程之间的配合,最后变成像素信息展示在显示器上。

image.png

总的来看,渲染任务经过了以下几个步骤:

  1. 解析html字符串,并在此阶段执行了js脚本
  2. 根据dom tree和cssom tree的信息进行样式计算
  3. 根据节点的样式,计算几何信息
  4. 分层,提高后续操作效率
  5. 通过布局信息,生成绘图指令
  6. 分块,为了优先绘制视口图像
  7. 合成线程利用GPU加速,通过光栅化生成位图信息
  8. 合成线程通过GPU中转,将位图信息交给显卡生成像素