《浏览器渲染原理深度解析:百度页面是如何渲染出来的?》

230 阅读9分钟

在浏览器中输入www.baidu.com 浏览器如何渲染出百度页面?

这篇文章不谈网络相关,我们只关注浏览器拿到html文档资源是如何解析的,如何绘制渲染页面。

微信图片_20240103120628.png

在浏览器百度页面打开查看源码,可以看到上图所示,它其实是一个html文档,核心是一个字符串。

而浏览器所做的事情就是把这字符串,解析成我们所看到的页面,这篇文章会从浏览器渲染的原理介绍页面渲染的每一个细节步骤

渲染时间点

微信图片_20240103122323.png

如上图所示,

  • 输入url,浏览器网络线程与服务器通信,拿到html文档。
  • 网络线程生产渲染任务,推入消息队列
  • 渲染主线程从消息队列中调度渲染任务开始执行(渲染时间点)

渲染流水线

渲染过程主要分为八个步骤、分别为:HTML解析、样式计算、布局、分成、光栅化、绘制、分块、画,每个步骤都会有响应的输入输出。

graph TD
HTML字符串 --> 解析html --> 样式计算 --> 布局 --> 分层 -->绘制  --> 分块 --> 光栅化 --> 画 --> 像素信息

HTML解析-ParseHTML

html字符串解析

微信图片_20240103133656.png

把html字符串通过ParseHTML转换成dom对象。

css解析(预解析线程)

微信图片_20240103134730.png

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

  • 内部样式表 (<style>
  • 外联样式表(<link ...>)
  • 行内样式表(<div style="">
  • 浏览器默认样式表

浏览器默认样式表在chrome源码中如图所示

微信图片_20240103141842.png

路径 chromium -> third_party ->blink -> renderer -> core -> html -> resources -> html.css

通过查看chrome默认样式源码:

div、p等元素为何独占一行,因为源码中明确设置了display:block

注意:js可以操作dom树也可操作上述样式表对象(StyleSheetList),但是没办法操作浏览器默认样式,可以在浏览器中输入 document.styleSheets 查看

document.styleSheets[0].addRule("div","border:2px solid #CCC") 在百度首页console中输入此行代码,可看到js操作styleSheets

HTML解析过程中遇到css代码怎么办?

通过上述分析得知,html和css的宏观解析逻辑。但是在主线程中是html代码一行一行解析执行的,如果在html解析的过程中遇到了css代码,浏览器应该如何处理呢?

为了提高解析效率,浏览器会启动一个预解析线程,率先下载和解析css

微信图片_20240103144502.png

  • 渲染主线程开始解析之前开启css预解析线程
  • 渲染主线程遇到link标签时,不会阻塞(css预解析线程处理)继续向前解析
  • 网络线程下载好css文件后交给css预解析线程处理
  • css预解析线程处理好之后推给渲染主线程
  • 渲染主线程生产cssom
  • 合并当前的dom和cssom

HTML解析过程中遇到JS代码怎么办?

渲染主线程遇到JS代码时必须暂停一切行为(因为js可能会改变dom树和cssom树),等在下载执行完后才能继续

预解析线程可以分担一点下载js的任务

微信图片_20240103145440.png

以上最终会得到两颗树 dom树 和cssom树

样式计算 - Reaclculate Style

将dom树和cssom树合并计算(计算后的样式)

样式计算-css计算过程

  • 样式基础
  • 层叠

样式计算-视觉格式化模型

  • 盒模型
  • 包含块

计算后的样式如图

微信图片_20240103150237.png

布局-Layout

主要算尺寸,位置,高度,宽度等。

dom树和layout树并不是一一对应的。比如display:none,那么layout树中就不会有这个节点。还有一些标签,可看浏览器默认样式表

位置

包含块知识 补充:布局树不是一个js对象,局部树是一个c++对象

分层-layer

由于页面不是静态的,渲染出来的页面是会变化的。分层就是用来更好处理这边变化页面的板块。可在浏览器中查看页面分层

和堆叠上下文有关的属性(z-index)会影响分层。

分层处理由浏览器自行处理,每个浏览器的处理办法不一样。

以百度首页为例

微信图片_20240103152750.png

绘制 -Paint

为每一个层生成单独的绘制指令

以一个绘制div为例

  • 将比移动某个板块
  • 画多宽多高
  • 填充颜色
  • ... 渲染主线程的工作就到此处结束了。剩余工作就交给其他线程完成。

小结主线程所做的工作

微信图片_20240103154218.png

分块-Tiling

分块会将每一层分为很多小的区域

微信图片_20240103154858.png

在mac电脑中的活动检测中可以查看到合成线程

微信图片_20240103155316.jpg

光栅化-Raser

光栅化就是将每一个块变成位图,

优先处理靠近视口的快。

位图其实就是每个像素点的信息(颜色)。

此过程会用到GPU加速。

微信图片_20240103161051.png

画-Draw

合成线程计算出每个位图在屏幕上的位置,交给GPU最终呈现。

注意css样式的旋转缩放在此阶段才确定。

为什么最后画的使用需要交给GPU去呈现,而不是合成线程直接呈现呢?

  • 合成线程在渲染进程中
  • 浏览器是以沙河的设计模式,可以和其他系统隔离
  • 合成线程是没法去直接调用其他应用程序的,只能通过GPU系统调用。
  • 这样设计更安全,浏览器遭受攻击时不会影响操作系统和其他程序。

面试

浏览器是如何渲染页面的?

参考答案

当浏览器的网络线程收到html文档后,会产生一个渲染任务,并将其传递给渲染主线程的消息队列。

在事件循环机制的作用下,渲染主线程取出消息队列中的渲染任务,开启渲染流程。

整个渲染流程分为多个阶段,分别是:HTML解析、样式计算、布局、分成、绘制、分块、画

每个阶段都有明确的输入输出,上一个阶段的输出会成为下一个阶段的输入。

这样,整个的渲染流程就形成了一套组织严密的生产流水线。

渲染的第一步是解析html。

解析过程中遇到css解析css,遇到js执行js。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载html中的外部css文件和外部js文件。

如果主线程解析到link位置,此时外部的css文件还没有下载好,主线程不会等待,继续解析后续的html。这是因为下载和解析css的工作是在预解析线程中进行的,这就是css不会阻塞html解析的根本原因。

如果主线程解析到script位置,会停止解析html,转而等待js文件下载好,并将全局代码解析执行完成后才会继续解析html。这是因为js代码的执行过程可能会修改当前的dom树,所以dom树的生成必须暂停。这就是js阻塞html解析的根本原因。

第一步完成后,会得到dom树和cssom树,浏览器的默认样式、外部样式、内部样式、行内样式均会包含在cssom树中。

渲染的下一步是样式计算。

主线程会遍历得到的dom树,依次为树中的每个节点计算出它最终的样式,称为Compute Style。 这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px

这一步完成后,会得到一颗带有样式的dom树。

接下来就是布局,布局完成后会的到布局树。

布局阶段会依次遍历dom树的每一个节点,计算每一个节点的几何信息。如节点的宽高、相对包含块的位置。

大部分时候,dom树和布局树并非一一对应。

比如 display:none的节点没有几何信息,因此不会生成到布局树;又比如使用了伪类元素选择器,虽然dom树中不存在这些伪类元素节点,但它们拥有几何信息,所以会生产到布局树中。还有匿名行盒、匿名快盒等等都会导致dom树和布局树无法一一对应。

下一步是分层

主线程会使用一套复杂的策略对整个布局树进行分层。

分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提高效率。 滚动条、堆叠上下文、transform、opacity等样式都会或多或少的影响分层结果,也可以通过will-change属性更大程度的影响分层结果

再下一步是绘制

主线程会为每一个层单独产生绘制指令集,用于描述这一层的内用该如何画出来。

分块-Tiling

完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。

和弦线程首先对每个图层进行分块,将其划分为更多的小区域。

它会从线程池中拿去多个线程来完成分块工作。

光栅化

合成线程会将快信息交给GPU进程,以极高的速度完成光栅化。

GPU进程会开启多个线程来进行光栅化,并且优先处理靠近视口区域的块。

光栅化的结果就是一块一块的位图

终于到了最后一个阶段了,合成线程拿到每一个层、每一个块的位图后生成指引信息。

指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转,缩放等变形。

变形发生在合成线程,与渲染线程无关,这就是transform效率高的本质原因。

合成线程会把指引星系提交高GPU进程,由GPU进程产生系统调用,提交GPU硬件,完成最终的屏幕成像。