开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第N天,点击查看活动详情
浏览器渲染原理
引言
第一个问题,什么是浏览器的渲染?
作为长期的电子设备使用者,我们已经习惯了各种页面的流畅显示,已经理所当然,觉得本就该那样。
但作为一名工程师,我们不应该以这样的角度观察我们的计算机,我们应该产生一个疑问--
我的电脑是怎么确定显示器上每个像素点的颜色的?
所谓的浏览器页面渲染,就是指浏览器将得到的html字符串经过一系列的处理加工,获取信息,最终告诉我们的显卡每个像素点的信息,从而显示出我们看到的网页。
提示: 理解本篇内容,需要对渲染主线程的事件循环机制有所了解,可以查阅资料或者看看我写的这篇文章: 【深入浏览器】事件循环机制详解
前奏
- 打开一个Tag(标签),浏览器启动一个渲染进程,渲染主进程开始执行
- 输入url地址按下回车,网络线程获取资源,拿到
html
资源 - 网络线程将 html 资源包装成渲染任务放入消息队列
- 渲染主线程通过事件循环机制拿到了渲染任务,开始执行渲染任务。
渲染任务
1. Parse HTML
渲染任务的第一步是 解析 html, 这一步的作用是,将 html 文本信息转换成后续方便操作的结构,即 dom 树 和 cssom 树。
并且,JS代码也在这个环节执行。
渲染主线程开始执行渲染任务,对获取到的html进行解析
在这个阶段,渲染主线程会根据信息将html文本转化为 DOM Tree 和 CSSOM Tree
DOM Tree
渲染主线程接到解析html的任务,开始着手解析
渲染主线程先创建 document
节点,接着扫描 html 文件,形成dom树
dom树的本质是引擎利用 html 信息生成方便后续操作的对象,js操作的dom对象是在此基础上的包装
dom树的特点是:
- document节点是根节点
- 每个节点只有一个父节点
CSSOM Tree
根节点 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,从而提升效率。
可以看到,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底层原理
该过程后,每个节点都有了确定的样式属性
3. LayOut
该阶段是布局,
根据每个节点的样式计算出每个节点的尺寸,位置
你可能不知道的细节:
这个步骤中所谓的位置是相对于包含块的位置,如果你还不清楚包含块的概念,请看我写的这篇文章:
LayOut 树和DOM树的结构不同的原因:
你可能观察上图注意到了,DOM树和布局树的结构似乎不相同,这是为什么呢?
原因1:几何信息
因为布局树需要得到每个元素的几何信息,而有些节点并没有几何信息,比如display none
的元素之所以不会显示在最终的页面上,也不会影响布局的原因,是布局树会忽略掉这些元素,比如 head 标签 的默认样式就是display: none
并且一些伪元素比如 ::after
因为有几何信息会出现在 layout 树中,所以伪元素影响布局
所以,dom树中虽然没有伪元素,但是布局树中有。
原因2:块盒和行盒的规则
布局的规则有以下两个原则:
- 内容必须在行盒中
- 行盒和块盒不能相邻
p节点不是行盒(浏览器默认样式), 而由于内容 "a"
需要在行盒中,所以在a节点的父节点新建了一个匿名行盒
b节点应该在匿名行盒中,但行盒不能和块盒相邻,因此会创建一个匿名块盒(这里图画错了)
JS是没有直接访问布局树的能力的,但底层为document对象暴露一些属性用于获取布局信息,比如 document.body.clientWidth
4. layers
该步骤是分层
分层的目的是为了提高后期操作页面的效率,只需要修改发生变化的层次,而不用修改整个页面
5. Paint
第五步是绘图
这里的绘图不是已经开始画像素点了,而是生成绘图指令,比如在哪那个坐标画一个什么样的图像,用什么颜色填充 (canvas与之类似的工作原理)
到此,渲染主线程的工作到此为止,渲染工作的其他部分交给浏览器的其他线程完成。
6. Tiling
第六步是分块
第五步结束后,渲染主线程将绘图指令信息交给合成线程。合成线程完成第六步分块任务。 合成线程为了提高效率,又会开启多个子线程进行分块。
分块是将每个层(第四步是分层)分为一些小块。分块的目的是,一般整个页面不会完全展示在显示器上,因此,将靠近视口的块优先绘制。
7.光栅化
第八步是光栅化,生成像素点信息,到了这一步,浏览器终于可以根据数据将文本转为像素信息
在这一步里,会用到GPU加速,合成线程光栅化的工作交给GPU进程,因为GPU进程效率更高,当GPU完成光栅化任务后,将位信息重新交给合成线程,合成线程利用信息形成位图。
将每个块转化成位图,优先处理靠近视口的块
8. draw
我们已经有了每个像素点的信息,终于可以在显示器上显示图像了。
第八步就是这个作用。
这一步又叫做draw quad,quad是辅助显卡确定像素点在显示器的具体位置的信息
合成线程利用GPU进程作为中转,将位图信息交给显卡硬件,最终显卡将具体的像素生成在显示器上,我们看到图像。
为什么合成线程不直接把位图信息交给硬件而是需要GPU进程中转呢?
这是因为合成线程和渲染主线程在渲染进程中执行的, 而渲染进程是在一个沙盒中,与外界是隔离的,目的是保证计算机的安全性,不至于网页调用硬件损坏电脑。
值得一提的是,css3中transform属性动画,是在第八步确定像素的,因此效率比较高。
总结
渲染过程实际上是一个流水线,每个阶段的输出是下一个阶段的输入,html字符串经过浏览器各种线程之间的配合,最后变成像素信息展示在显示器上。
总的来看,渲染任务经过了以下几个步骤:
- 解析html字符串,并在此阶段执行了js脚本
- 根据dom tree和cssom tree的信息进行样式计算
- 根据节点的样式,计算几何信息
- 分层,提高后续操作效率
- 通过布局信息,生成绘图指令
- 分块,为了优先绘制视口图像
- 合成线程利用GPU加速,通过光栅化生成位图信息
- 合成线程通过GPU中转,将位图信息交给显卡生成像素