大致过程

  • 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  • 然后,在网络进程中发起真正的 URL 请求。
  • 接着网络进程接收到响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  • 浏览器进程接收到网络进程的响应头数据之后,发送 “提交导航( CommitNavigation )” 消息到渲染进程。
  • 渲染进程接收到 “提交导航” 的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道。
  • 最后渲染进程会向浏览器进程 “确认提交” ,这是告诉浏览器进程:“已经准备好接受和解析页面数据了” 。
  • 浏览器进程接收到渲染进程 “提交文档” 的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

具体过程

用户输入

当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带关键字的 URL。
  • 如果输入内容符合 URL 规则,那么地址栏会根据规则加上协议,合成完整的 URL。

随后当用户键入回车之后,浏览器即将开始加载一个地址,不过在此之前会有一次执行 beforeunload 事件的机会,可以通过此事件来取消导航,如果没有监听这个事件或者同意了继续后续流程,浏览器便开始加载一个地址。

开始加载后,标签页上的图表便进入了加载状态,此时页面显示的还依然是之前打开的内容,这是因为需要等待提交文档阶段,页面内容才会被替换。

URL 请求过程

到了页面资源请求过程。这时浏览器进程会通过(IPC)把 URL 请求发送至网络进程,在网络进程中发起真正的 URL 请求。

首先,网络进程会查找本地缓存,查到就直接返回资源给浏览器进程,没查到那么直接进入网络请求流程。

1.DNS

进行 DNS 解析,以获取服务器 IP 地址。

DNS 查询顺序:

  1. 浏览器的 DNS 缓存
  2. 操作系统 DNS 缓存
  3. hosts 文件
  4. DNS 服务器

2.建立连接

利用 IP 地址和服务器建立 TCP 连接,如果是 HTTPS 协议那么在 TCP 连接之后还要进行 TLS 连接。

3.发送请求

连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

4.接收响应

服务器接收请求信息后,会根据请求信息生成响应数据(包括响应行、响应头、响应体等信息),并发送给网络进程。网络进程接收了响应信息后就开始解析响应头的内容。

如果响应状态码包含了 301 或者 302 一类的跳转信息,那么网络进程会从响应头的 Location 字段里读取重定向的地址,然后重投开始一遍。

浏览器会根据响应头中的 Content-Type 来选择如何处理该响应。如果是 HTML 那么继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,接下来就需要准备渲染进程了。

准备渲染进程

默认情况下 Chrome 会为每个页面分配一个渲染进程,但也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程。

打开一个新页面采用的渲染进程策略:

  • 通常情况下,打开的新的页面都会使用单独的渲染进程。
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

提交文档

提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程

  1. 浏览器进程向渲染进程发起 “提交文档” 的消息
  2. 渲染进程收到 "提交文档" 的消息后,会和网络进程建立传输数据的 “管道”
  3. 文档数据传输完成之后,渲染进程会返回 “确认提交” 的消息给浏览器进程
  4. 浏览器进程收到 “确认提交” 的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面

渲染阶段

一但文档被提交,渲染进程便开始页面解析和子资源加载了,一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。

渲染流程

由于渲染机制过于复杂,所以渲染模块在执行过程中会被划分为很多子阶段,输入的 HTML 经过这些子阶段,最后输出像素。这个过程叫做渲染流水线。

按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

每个子阶段都会有以下三部分内容:

  • 开始每个子阶段都有其输入内容。
  • 然后每个子阶段都有其处理过程。
  • 最终每个子阶段都会产生输出内容。

构建 DMO 树

浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树

构建 DOM 树的输入内容是一个 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM。

document 结构就是 DOM 结构,与 HTML 不同的是,DOM 是保存在内存中的树状结构,可以通过 JavaScript 来查询或修改其内容。

样式计算

计算出 DOM 节点中每个元素的具体样式,这个阶段大致可分为三步

1.把 CSS 转换为浏览器能够理解的内容

CSS 的来源主要有三种:

  • 通过 link 引用的外部 CSS 文件

  • <style> 标签内的 CSS
  • 元素 style 属性的内联样式

渲染引擎也会将 CSS 文本转换为浏览器可以理解的结构——styleSheets。

2.转换样式表中的属性值,使其标准化

属性标准化是指:例如 2em、blue、bold,这些类型数值会被转换为渲染引擎可以理解的、标准化的计算值

3.计算出 DOM 树中每个节点的具体样式

计算过程需要遵循 CSS 的继承和层叠两个规则

层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法,它在 CSS 处于核心地位,CSS 的全称 ”层叠样式表“ 正是强调了这一点。

这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。

布局阶段

计算出 DOM 树种可见元素的几何位置

1.创建布局树

构建一颗只包含可见元素布局树

  • 遍历 DOM 树中所有可见节点,并把这些节点加到布局树种
  • 不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容、display:none
2.布局计算

有了一颗完整的布局树,接下来会计算布局树节点的坐标位置(后续补充计算过程)。

在执行布局操作时,会把布局运算的结果重新写回到布局树种,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段没有清晰的将输入内容和输出内容区分开来。针对这个问题,Chrome 团队在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

分层

页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便的实现这些效果,渲染引擎还需要为特定的节点产生专用的图层,并生成一颗对应的图层树(LayerTree)。渲染引擎给页面分了很多图层,这些图层按照一定的顺序叠加在一起,就形成了最终的页面。

通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。但不管怎样,最终每一个节点都会直接或者间接的从属于一个层。

渲染引擎为特定节点创建新的图层,通常满足下面两点中任意一点:

  1. 拥有层叠上下文属性的元素会被提升为单独的一层。

    页面是个二维平面,但是层叠上下文能过够让 HTML 元素具有三维概念,这些 HTML 元素按照自身属性的优先级分布在垂直于这个二位平面的 z 轴上。

    明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。

  2. 需要裁剪(clip)的地方也会被创建为图层。

    当元素内部的元素内容超出元素区域时就产生了裁剪,渲染引擎会把裁剪内容的一部分用于显示在元素区域,出现这种裁剪情况的时候,渲染引擎会为超出元素单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。

图层绘制

渲染引擎会将一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。绘制列表中的指令非常简单,就是让其执行一个简单的绘制操作,比如绘制粉色矩形或者黑色的线等。而绘制一个元素通常需要好几条绘制指令,因为每个元素的背景、前景、边框都需要单独的指令去绘制。在图层绘制阶段,输出的内容就是这些待绘制列表。

栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。当涂层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。

合成线程会把图层划分为图块(tile),这些土块的大小通常是 256 * 256 或者 512 * 512。然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,生成的位图被保存在 GPU 内存中。

GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。渲染进程把生成图块的指令发送给 GPU,然后再 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

合成和显示

一旦所有的图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容会知道内存中,最后再将内存显示在屏幕上。

到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出页面了。