浏览器的页面渲染流程

280 阅读32分钟

学习前端也有几个月了,对于浏览器的理解也仅停留在输入url可以显示网页,可以用于调试JavaScript程序等等。自从看到Mariko KosakaInside look at modern web browser 这篇博客,完全改变了我对浏览器的认知,特地在此记录本人对该系列博客的学习以及一些理解。

一、浏览器架构

首先复习几个计算机中的基础概念:

  • 中央处理器(central processing unit, CPU) 在计算机体系结构中,CPU 是对计算机的所有硬件资源(如存储器、输入输出单元) 进行控制调配、执行通用运算的核心硬件单元。CPU 是计算机的运算和控制核心。计算机系统中所有软件层的操作,最终都将通过指令集映射为CPU的操作。
  • 图形处理器(Graphics Processing Unit, GPU) GPU 又称为显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备上用于图像运算微型处理器。GPU专门执行复杂的数学和几何计算。目前GPU对于普通用户的作用一般为2D或3D图形的加速,但GPU实际在浮点运算、并行计算等部分计算方面,已经能够提供数十倍乃至数百倍于CPU的性能。
  • 进程(Process) 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
  • 线程(thread) 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

换句话说,当启动一个应用程序时,会创建一个进程,操作系统会为该进程分配一块独立的内存空间,当应用程序关闭时,该进程中止,分配的内存也会被操作系统释放。而线程则是负责该进程中应用程序不同部分的执行,一个进程至少有一个线程,也可以有多个线程,多个线程之间共享数据。

1.1 浏览器中的进程

浏览器中主要包含以下四种进程:

  • 浏览器进程(Browser Process) 控制应用程序的“chrome”部分,包括地址栏、书签、后退、前进按钮。还处理网络浏览器中不可见的特权部分,例如网络请求和文件访问。
  • 渲染进程(Renderer Process)—— 浏览器内核 也成为浏览器内核,控制显示网站的标签页内的任何内容。

Chrome会为每个标签页提供一个进程,但是为了节省内存,Chrome 限制了它可以启动的进程数。限制取决于您的设备拥有多少内存和 CPU 能力,但是当 Chrome 达到限制时,它会开始在一个进程中运行来自同一站点的多个选项卡

如果所有标签页都在一个进程上运行,当一个标签页无响应时,所有标签页均无响应,这样用户体验极差。而每个选标签页都由一个独立的渲染器进程运行,当一个标签页无响应时,可以关闭无响应的标签页并继续前进,同时保持其他选项卡处于活动状态。

  • 插件进程(Plugin Process) 控制网站使用的任何插件。例如,Flash插件。
  • GPU进程(GPU Process) 与其他进程隔离处理 GPU 任务。它被分成不同的进程,因为 GPU 处理来自多个应用程序的请求并将它们绘制在同一个表面上

image.png

1.2 查看Chrome浏览器进程

我们可以点击浏览器右上角的三点图标,选择更多工具中任务管理器查看浏览器中的进程。

image.png 上图是我浏览器中的进程情况,可以看到

  • 浏览器进程 image.png
  • GPU进程 image.png
  • 渲染进程 Chrome 的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点(拥有相同协议和根域名)的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 process-per-site-instance。即:
  • 默认情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

image.png

  • 插件进程 image.png

1.3 Chrome中的服务化

Chrome 准备将将浏览器程序的每个部分作为服务运行,从而可以轻松拆分为不同的进程或聚合为一个。

一般的想法是,当 Chrome 在强大的硬件上运行时,它可能会将每个服务拆分为不同的进程以提供更高的稳定性,但如果它在资源受限的设备上,Chrome 会将服务整合到一个进程中以节省内存占用

例如下图的这些进程就Chrome面向服务化的架构:

image.png

1.4 渲染进程(浏览器内核)

(1)浏览器内核种类

  • 1、Trident Trident(IE内核),是微软开发的一种排版引擎。
  • 2、Gecko Gecko(Firefox内核):Netscape6开始采用的内核,后来的Mozilla FireFox(火狐浏览器) 也采用了该内核,Gecko的特点是代码完全公开,因此,其可开发程度很高,全世界的程序员都可以为其编写代码,增加功能。
  • 3、Presto Presto(Opera前内核) (已废弃): Opera12.17及更早版本曾经采用的内核,现已停止开发并废弃,该内核在2003年的Opera7中首次被使用,该款引擎的特点就是渲染速度的优化达到了极致,然而代价是牺牲了网页的兼容性。
  • 4、Webkit Webkit(Safari内核,Chrome内核原型,开源): 它是苹果公司自己的内核,也是苹果的Safari浏览器使用的内核。
  • 5、Blink Blink是一个由Google和Opera Software开发的浏览器排版引擎

有关浏览器内核详细介绍可见这里

(2)渲染进程(浏览器内核)组成

渲染进程(浏览器内核)是多线程的,主要由以下线程组成:

  • 渲染引擎线程 负责解析HTMl、CSS,构建DOM树、渲染树、布局、绘制等。首次加载页面或者某种操作引发回流(Reflow)和重绘(Repaint)时,该线程就会执行。
  • JS引擎线程(JS内核) JavaScript 引擎是一个执行 JavaScript 代码的程序或解释器。例如:Google开发的V8引擎(C++编写)。

注意:\color{red}{注意:} 渲染线程和JavaScript引擎线程是互斥的(因为JavaScript中可以操作DOM),当JavaScript引擎执行时,渲染线程就会被挂起,这也是为什么JavaScript会阻塞解析。

  • 事件触发线程 用于控制事件循环。- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
  • 定时器触发线程 setInterval与setTimeout所在的线程,浏览器定时计数器并不是由JavaScript引擎计数的,而是由该线程执行的。
  • 异步HTTP请求线程 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行 上述渲染进程组成参考自该篇博客

image.png

二、导航 —— 从输入URL到页面展示

导航是加载 web 页面的第一步。它发通常生在用户通过在地址栏输入一个 URL(点击链接跳转页面其实也是更改了地址栏的输入)。即用户发出URL请求到页面开始解析的这个过程。

image.png
从用户在地址栏输入URL到页面展示,一般会经过如下几个阶段

2.1 用户输入

当在地址栏中键入一个查询关键字时,由浏览器进程的 UI 线程处理该输入。在Chrome中,地址栏也是一个搜索输入区域。所以,当用户开始在地址栏输入内容时,UI线程线程需要解析是搜索内容还是请求URL。

  • 如果是搜索内容:地址栏会被使用浏览器默认的搜索引擎来合成新的带搜索关键字的URL
  • 如果是URL:地址栏会根据规则,把这段内容加上协议,合成为完整的URL。 用户在地址栏输入关键字并回车后,意味着当前页面要被替换成新的页面。但在这之前会触发beforeunload事件,允许页面在退出之前执行一些数据清理工作,或者询问用户是否离开当前页面。所以,可以通过beforeunload事件取消导航,让浏览器不再执行任何后续工作。

2.2 开始导航

处理好用户输入之后,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网路进程接收到URL后,发起请求,获取站点内容。
网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么按以下步骤进行网络请求。

(1)DNS查询

导航的第一步要根据输入的URL寻找页面资源的位置。这个位置就是存放资源的就是服务器,通过IP地址标识。如果输入的是IP地址,那么就直接定位到IP地址对应的服务器上。如果输入的是域名,那么就要按以下步骤进行DNS域名解析,最终得到一个IP地址。

DNS(Domain Name Server,域名服务器):是进行域名(domain name)和与之相对应的IP地址 (IP address)转换的服务器。DNS中保存了一张 域名(domain name) 和与之相对应的 IP地址 (IP address) 的表,以解析消息的域名。

  1. 用户在 Web 浏览器中键入 “example.com”,查询传输到 Internet 中,并被 DNS 递归解析器接收。
  2. 接着,解析器查询 DNS 根域名服务器(.)。
  3. 然后,根服务器使用存储其域信息的顶级域(TLD)DNS 服务器(例如 .com 或 .net)的地址响应该解析器。在搜索 example.com 时,我们的请求指向 .com TLD。
  4. 然后,解析器向 .com TLD 发出请求。
  5. TLD 服务器随后使用该域的域名服务器 example.com 的 IP 地址进行响应。
  6. 最后,递归解析器将查询发送到域的域名服务器。
  7. example.com 的 IP 地址而后从域名服务器返回解析器。
  8. 然后 DNS 解析器使用最初请求的域的 IP 地址响应 Web 浏览器。
  9. DNS 查找的这 8 个步骤返回 example.com 的 IP 地址后,浏览器便能发出对该网页的请求: image.png
    此外,为了更快解析DNS查询,避免额外查询,从而缩短加载时间并减少带宽/CPU 消耗。DNS 数据可缓存到各种不同的位置上,且每个位置均将存储 DNS 记录并保存由生存时间(TTL)决定的一段时间。
  • 浏览器DNS缓存 在 Chrome 浏览器中,您可以转到 chrome://net-internals/#dns 查看 DNS 缓存的状态。 image.png
  • 系统host文件 可通过本地路径 C:\Windows\System32\drivers\etc\hosts访问该文件,该文件中存放了访问过的域名到IP地址的映射 image.png
  • 本地DNS解析器缓存 所以在向域名服务器发送请求前,会先按浏览器DNS缓存-》系统host文件-》本地DNS解析器缓存的步骤查看缓存。

此外, DNS占用53号端口,同时使用TCP和UDP协议DNS在区域传输的时候使用TCP协议,其他时候如域名解析时使用UDP协议

如果想深入了解,可阅读什么是 DNS? | DNS 的工作方式多张图带你彻底搞懂DNS域名解析过程

(2)TCP 连接

得到服务器的IP地址后,浏览器就会通过TCP协议与服务器建立连接

这里简要介绍以下TCP三次握手建立连接的步骤:

  • 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
  • 第二次握手:服务器收到syn包,必须确认客户端的syn(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 三次握手的目的是保证双方都有发送和接收的能力。 tcp_connect.gif
    这里顺带补充一下TCP四次挥手关闭连接的知识: 由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
  • 第一次挥手:HostA 发送一个 FIN,用来关闭 HostA 到 HostB 的数据传送。
  • 第二次挥手:HostB 收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
  • 第三次挥手:HostB 关闭与 HostA 的连接,发送一个FIN给 HostA。
  • 第四次挥手:HostA 发回 ACK 报文确认,并将确认序号设置为收到序号加1

TCP采用四次挥手关闭连接,为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

(3)SSL/TLS协商

如果是在HTTPS上建立安全连接,那么就要通过SSL/TLS协议建立安全连接。基本流程如下:

  • 客户端向服务器索要并验证服务器的公钥。
  • 双方协商产生会话密钥
  • 双方采用会话密钥进行加密通信 ssl-setup-diagram.png

(4)发送HTTP请求

浏览器建立了与web服务器的连接之后,浏览器就可以代表用户发送一个初始的 HTTP 或 HTTPS 请求。

(5)服务器处理请求并返回响应结果

服务器收到请求,进行处理,使用相关的响应头和数据进行回复。

2.3 处理响应

(1)重定向

在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 、302 一类的跳转信息,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,重新导航到新地址。
但如果响应头的状态码是 200 ,那么表示浏览器可以继续处理该请求。

(2)响应数据类型处理

响应头中有个重要的字段 Content-Type ,告诉浏览器服务器返回的响应体数据是什么类型。

  • 如果响应是一个 HTML 文件,那么下一步就是将数据传递给渲染进程
  • 但如果它是一个 zip 文件或其他文件,那么这意味着它是一个下载请求,所以他们需要将数据传递给下载管理器(导航流程在此也就结束了 )。

(3)安全检查

这也是进行安全浏览检查的地方。如果域和响应数据似乎与已知的恶意站点相匹配,则网络线程会发出警报以显示警告页面。此外,发生 Cross Origin Read Blocking(CORB) 的跨站点数据不会进入渲染器进程。 image.png

2.4 查找渲染进程

一旦完成所有检查并且网络线程确信浏览器应该导航到请求的站点,网络线程就会告诉 UI 线程数据已准备好。UI线程然后找到一个渲染器进程来进行网页的渲染
由于网络请求可能需要数百毫秒才能得到响应,所以当 UI 线程在向网络线程发送 URL 请求时,UI 线程就尝试主动查找或启动与网络请求并行的渲染进程。这样,如果一切按预期进行,当网络线程接收到数据时,渲染进程已经处于待机位置。如果导航重定向跨站点,则可能不会使用此备用进程,在这种情况下,可能需要不同的进程。 image.png 渲染进程准备好后,还不能开始渲染页面,因为此时文档数据还在网络进程中,还没有提交给渲染进程。

2.5 提交导航

数据和渲染进程准备就绪后,浏览器进程就将网络进程接收到的 HTML 数据提交给渲染进程

  • 浏览器进程接收到网络进程的响应头数据后,便向渲染进程发起“提交导航”的消息
  • 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
  • 浏览器进程在收到“确认提交”的消息后会:
    • 地址栏更新
    • 安全指示器和站点设置 UI 反映了新页面的站点信息。
    • 该选项卡的会话历史记录将被更新,因此后退/前进按钮将逐步浏览刚刚导航到的站点。
    • 为了在关闭选项卡或窗口时能恢复选项卡/会话,会话历史记录存储在磁盘上。 image.png

如果导航是从渲染器进程启动的(例如用户单击链接或客户端 JavaScript 已运行window.location = "https://newsite.com"),则渲染器进程首先检查beforeunload处理程序。然后,它经历与浏览器进程启动导航相同的过程。唯一的区别是导航请求是从渲染器进程启动到浏览器进程的。

三、渲染流程

渲染进程负责标签页内发生的所有事情,核心工作是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页

3.1 解析(Parsing)

image.png

3.1.1 解析HTML

(1)构建 DOM 树

当渲染进程接收到导航的提交信息并开始接收HTML数据时,主线程开始解析超文本字符串(HTML)并将其转换为文档对象模型(DOM)。 image.png 如上图,主要经历了一下几个步骤:

  • Conversion:浏览器从磁盘或网络读取HTML原始字节,并根据文件的指定编码(例如:UTF-8)将它们转换成单个字符。
  • Tokenizing:将字符串转换成 W3C HTML5 标准规定的 Token,每个 Token 都具有特殊含义和自己的一组规则。(该步骤通过状态机实现)

image.png

  • Lexing: Token 转换成定义了属性等规则的“对象”
  • DOM construction:最后,因为 HTML 标记定义了不同标记之间的关系(一些标记包含在其他标记中),所以创建的对象链接在一个树数据结构中,该结构还捕获了原始标记中定义的父子关系:HTML对象是正文对象的父对象,正文段落对象的父对象,依此类推。(该步骤通过栈实现) 详细过程可参考 DOM 树的构建

(2)加载子资源

网页中通常包含图像、web字体、CSS 和 JavaScript等外部资源,这些文件需要从网络或缓存中加载。主线程在解析构建DOM树的过程中找到外部资源会请求它们。但是,这样会浪费时间,所以为了提高效率,预加载扫描器会并发运行,在后台检索资源,以便在主 HTML 解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。

例如,HTML 文档中有<img>、<link>等标签,预加载扫描器会查看HTML解析器生成的 token,并向浏览器进程中的网络线程发送请求。

(3)JavaScript 会阻塞 HTML 解析

当 HTML 解析器找到一个<script>标签时,它会暂停 HTML 文档的解析,并且必须加载、解析和执行 JavaScript 代码。 (所以 <script> 标签最好放在body底部)
之所以这样,是因为JavaScript中document.write()之类的操作会改变整个 DOM 树的结构,从而改变整个文档的形式。下图出自HTML Standard,对上述是一个很好的解释
image.png
当 JavaScript 的加载和执行顺序不重要,且JS代码中没有修改DOM的结构,可以通过给<script>标签添加asyncdefer 属性,让浏览器在后台异步加载 JavaScript 代码,并且不会阻塞 DOM 构建和页面呈现。

  • defer 属性:异步加载JavaScript文件,在HTML解析完之后去执行,但在DOMContentLoaded事件之前执行。能够保证脚本将按照它们在 HTML 中出现的顺序执行并且不会阻塞解析器。
  • async 属性:异步加载JavaScript文件,加载完成后就立即执行。这意味着 async 脚本可能(并且很可能)不会按照它们在 HTML 中出现的顺序执行。这也意味着如果它们在解析器仍在工作时完成下载,会中断 DOM 构建。 image.png
    此外,可以通过<link rel="preload">声明预加载链接,以指示浏览器尽快下载关键资源。要想了解更多,可以阅读资源优先级相关文档。

注意:\color{red}{注意:}只有加载JavaScript才又可能阻塞HTML解析,加载其他外部资源不会阻塞HTML解析。

3.1.2 样式计算(Recalculate Style)

image.png DOM 树捕获了标签的属性和关系,但并没有告诉我们元素呈现时的外观,这是在CSS中设置的。主线程会解析CSS并确定每个 DOM 节点的计算样式,通常按以下步骤来完成:

  • (1) 将CSS转换成浏览器能够理解的结构 CSS样式的来源主要有三种:
  1. 通过 link 引用的外部 CSS 文件
  2. <style> 标记内的 CSS元素的
  3. style 属性内嵌的 CSS 与 HTML 一样,浏览器无法直接理解纯文本的CSS样式, 所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets
  • (2) 转换样式表(styleSheets)中的属性值,使其标准化 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。例如下图:

image.png

  • (3) 计算 DOM 树种每个节点的具体样式 DOM 树中每个节点样式的计算,主要遵循CSS的继承规则和层叠规则:
  1. 继承规则: CSS 继承就是每个 DOM 节点都包含有父节点的样式(但不是所有属性都是可继承的)。
  2. 层叠规则: 一个定义了如何合并来自多个源的属性值的算法. 简单来说,就是CSS中对于DOM节点定义的多个样式,哪个会真正起作用. 每个DOM元素最终的计算样式,可在Chrome的开发者工具,"element"标签的"Computed"子标签中查看.

image.png

构建 CSSOM 树

对于样式计算, 也可做如下解释。构建 DOM 后,浏览器从所有来源读取 CSS 并构建一个CSSOM。CSSOM 代表CSS 对象模型,它是类似于 DOM 的树状结构。

此树中的每个节点都包含 CSS 样式信息,这些信息将应用于它所针对的 DOM 元素(由选择器指定)。然而,CSSOM 不包含无法在屏幕上打印的 DOM 元素,例如<meta>,<script><title>

image.png CSS 字节被转换为字符,然后是标记,然后是节点,最后它们被链接成一个称为“CSS 对象模型”(CSSOM)的树结构

image.png

默认样式

就算没有提供任何CSS,每个DOM节点也有一个计算样式.例如:字体大小\边距等. 有关Chrome 的默认 CSS 是什么样的,可以查看源代码

image.png

CSS会阻塞渲染

前面说到,请求CSS等非阻塞外部资源不会阻塞DOM的解析过程。

DOM 和 CSSOM 树的构造都发生在主线程上,并且这些树是同时构造的。但不同的是,DOM 树的生成是增量的,这意味着当浏览器读取 HTML 时,它会将 DOM 元素添加到 DOM 树中。但 CSSOM 树的情况并非如此。与 DOM 树不同,CSSOM 树的构建不是增量的,必须以特定的方式进行。这是因为文件末尾的 CSS 规则可能会覆盖文件顶部的 CSS 规则。

因此,如果浏览器在解析样式表内容时开始逐步构建 CSSOM,则会导致渲染树的多次渲染,因为样式表文件中稍后会出现样式覆盖规则,因为相同的 CSSOM 节点正在更新。CSS 被解析时,看到元素在屏幕上改变样式将是一种不愉快的用户体验。所以浏览器不会增量处理外部 CSS 文件,并且 CSSOM 树更新会在样式表中的所有 CSS 规则被处理后才会发生。

CSS 是一种渲染阻塞资源。一旦浏览器发出获取外部样式表的请求,渲染树的构建就会停止。因此,关键渲染路径CRP ) 也卡住了,屏幕上没有渲染任何内容,如下所示。但是,在后台下载样式表时,DOM 树的构建仍在进行中。

3.2 构建渲染树(Render Tree)

基于 HTML 和 CSS 输入构建了 DOM 和 CSSOM 树后,浏览器将 DOM 和 CSSOM 组合成一个“渲染树”,它捕获页面上所有可见的 DOM 内容以及每个节点的所有 CSSOM 样式信息。 image.png 为了构建渲染树,浏览器大体上完成了下面这些工作:

  1. 从 DOM 树的根开始,遍历每个可见节点
    • 一些节点是不可见的(例如,脚本标签、元标签等),并且被省略,因为它们不会反映在渲染的输出中。
    • 一些节点通过 CSS 隐藏,也从渲染树中省略;例如,设置了“display:none”属性的节点
  2. 对于每个可见节点,找到适当的匹配 CSSOM 规则并应用它们。

3.3 布局(Layout)

现在渲染进程知道了文档的结构和每个 DOM 节点的样式了,但这还不足以渲染页面,因为DOM元素的几何信息还不知道。

布局是确定元素几何形状的过程,即确定所有 DOM 节点的大小和位置

布局是一个递归过程——它从根节点开始,即HTML 文档的<html>元素。布局通过部分或整个 渲染树层次结构递归地继续,为每个需要它的DOM节点计算几何信息。

根节点的位置为0,0,尺寸为浏览器窗口可见部分(也称为视口)的大小。

布局的详细过程可参考这篇博客

此外,第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。

3.4 绘制(Paint)

分层

拥有了DOM、样式和布局了,但这仍然不足以呈现页面。
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,主线程会遍历布局树,并生成一棵对应的图层树(LayerTree),根节点是与页面中根节点对应的图层。

但并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。一般以下几种情况会创建新的图层:

  • 拥有层叠上下文属性的元素
  • 嵌入 <video>、<canvas>、Silverlight 或 Flash 等插件(在特殊情况下)

image.png

绘制顺序

图层树构建好了,还必须判断绘制它们的顺序。例如,某些元素设置了z-index属性,在这种情况下,按照 HTML 中编写的元素顺序绘制将导致不正确的呈现。

image.png
在此绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是“先背景,后文字,后矩形”的绘画过程的记录。

image.png 具体的绘制顺序,可见CSS2中定义的顺序

3.5 合成(Compositing)

合成是一种技术,可以将页面的各个部分分成图层,分别将它们光栅化,然后在称为合成线程的单独线程中合成为页面。如果发生滚动,由于图层已经被光栅化,它所要做的就是合成一个新的帧。可以通过移动图层并合成新帧以相同的方式实现动画。

光栅化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。一旦创建了图层树并确定了绘制顺序,主线程就会将该信息提交给合成线程。紧接着合成线程会进行如下操作:

  • (1)划分图块(tile) 通常一个页面可能很大,但是用户只能看到其中的一部分,而用户能够看到的部分称为视口(viewport)。有些情况下,有的图层可能很大,超过了视口的范围(例如有些页面需要滚动才能查看全部)。但是用户只能看到视口范围内的部分,所以这种情况下如果绘制图层所有内容,不仅开销很大,而且没有必要。

对此,合成线程会将图层划分为图块(title),这些图块的大小通常是 256x256 或者 512x512,如下图所示:

image.png

  • (2)光栅化(raster) 划分好图块后,合成线程会对视口(或附近)内的图块优先生成位图(bitmap),但实际生成位图的操作是由光栅线程执行的。

所谓光栅化,就是将图块(tile)转换成位图(bitmap)。图块是光栅化执行的最小单位。

渲染进程维护了一个光栅化的线程池,所有图块的光栅化都是在线程池中执行的,如下图所示: image.png 通常情况下,光栅化过程会使用GPU来加速,使用GPU生成位图的过程叫做快速光栅化或GPU光栅化,生成的位图保存在GPU的内存中。(这里跨进程操作了)

image.png

合成并显示

当图块所有图块都被光栅化后,合成线程会收集称为 draw quads 的图块信息去创建一个合成框架(compositor frame)

raw quad:包含的 块在内存中的位置,以及页面合成时图块绘制的位置 等信息。
mpositor frame示一个页面的 Draw quad 集合。

然后通过进程间通信(IPC)将合成框架(compositor frame)提交给浏览器进程,此时,可以从 UI 线程添加另一个合成框架以更改浏览器 UI,或从其他渲染进程添加用于扩展。这些合成帧被发送到 GPU 以在屏幕上显示。如果出现滚动事件,合成器线程会创建另一个合成帧以发送到 GPU。有关Chrome中GPU加速合成可见这篇文章

image.png 合成的好处是它是在不涉及主线程的情况下完成的。合成器线程不需要等待样式计算或 JavaScript 执行。

好啦,到此浏览器就能展示出完美的页面啦!

小结

上述对浏览器的渲染流程有了大致的介绍,但是由于浏览器的区别以及浏览器的更新迭代,在某些步骤上可能会存在差别。最后也对渲染流程做个小小的总结:

(1)解析(Parsing)

  • 解析 HTML:主线程解析HTML,并构建DOM树
  • 解析 CSS:主线程解析 CSS,进行样式计算,确定每个DOM节点的样式,生成CSSOM树 (2)合成 RenderTree
    DOM树 和 CSSOM树构建好后,将DOM树 和 CSSOM树合成为 RenderTree,RenderTree中只包含可见节点

(3)布局(Layout)
确定DOM节点的几何形状,即确定DOM节点的大小和位置。(得到布局树)

(4)绘制(Paint)

  • 对布局树进行分层,生成图层树(LayerTree)。
  • 为每个图层生成绘制列表,提交到合成线程。

(5)合成(Composition) 合成线程将图层划分为图块(tile),并在光栅化线程池中将图块转换成位图。所有图块转换成位图后, 发送给浏览器进程,浏览器进程生成页面并显示。

image.png

以上步骤中可能存在不严谨的地方,在学习过程中其实很多地方还是没完全明白,例如:

  • 渲染树(Render Tree)和布局树(Layout Tree)区别在哪,个人理解是渲染树进行布局计算后得到了布局树,不知道对不对
  • 官方文档中还看到RenderObject、RenderLayer、GraphicLayer等术语,感觉自己在很多细节方面还是没有理解透彻。 所以,上述流程中存在问题的地方,希望大佬们可以提出指正,也希望大佬们可以解疑。

四、渲染性能

渲染性能一直是前端工程师关注的一个重点,这里借着学习渲染流程,提几个与渲染性能相关的概念。

4.1 回流(Reflow)

回流(Reflow) 也称为重排,指的是重新计算页面的布局。回流会重新计算元素的大小和位置,并且还会触发该元素的子元素、祖先元素和出现在 DOM 中的元素的进一步回流,然后进行重绘。

回流(Reflow)操作开销很大,但它却很容易被触发,例如以下情况:

  • 插入、删除或更新 DOM 元素
  • 移动 DOM 元素,或给 DOM 元素添加动画
  • 修改页面上的内容,例如:修改输入框中的文本
  • 改变页面大小(reseize)
  • 滚动页面(scroll)
  • 添加或删除样式表(styleSheet)
  • 更改元素的类名(clssName)
  • 改变字体大小(font-size)
  • 激活伪类
  • 对一个元素进行测量,如offsetHeight或getComputedStyle ……

如下图,如果触发了回流,就需要重新布局页面,任何受影响的区域需要重新绘制,并且最终绘制的元素需要重新组合在一起。所以,重排需要更新完整的渲染流水线,开销很大,要尽量避免。

image.png

4.2 重绘(Repaint)

重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。例如背景图像、文本颜色或阴影等。

如下图所示,重绘浏览器会跳过布局阶段,所以执行效率会比回流操作要高一些。 image.png

4.3 重新合成

如果修改了一个既不需要布局也不需要绘制的属性,渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。例如:transform属性。

image.png 这种操作效率是最高的,不仅跳过了布局和绘制两个阶段,而且后续合成的一些操作都不是在主线程上进行的,不会占用主线程资源。

有关在渲染流程的每个步骤中优化渲染性能的方法,可以参考这个博主的系列博客

看了好多天,终于大概了解了浏览器是干啥的了,本文到此也就结束啦。后续接着去对自己疑惑的地方再学习学习。

参考
[1] How Browsers Work: Behind the scenes of modern web browsers
[2] www.youtube.com/watch?v=SmE…
[3] Inside look at modern web browser
[4] 浏览器工作原理与实践

这里只列出了主要参考的文章和视频,其他一些参考也在文章中通过链接形式给出,这里就不一一列出啦。这篇文章是站在大佬的肩膀上的学习记录,其中存在错误的地方希望大佬们可以提出指正呀。