从输入URL到页面展示,这中间发生了什么?

前言

在浏览器输入URL到页面展示,这中间发生了什么?

这是一道经典的题目,学习了李兵大佬的浏览器工作原理,整理了一下学习笔记。

导航流程

用户发出URL请求到页面开始解析的过程,就叫导航流程。

浏览器进程接收用户输入的URL请求,再将该URL转发给网络进程

用户输入URL/查询关键词

浏览器会通过导航栏的查询关键词或URL,根据逻辑规则生成完整的URL地址。

现代的浏览器搜索栏都有内置搜索引擎,当用户输入的是搜索关键词(即不符合URL规则),则会根据用户设置的浏览器默认搜索引擎,合成带查询关键词的URL。

1
2
# 输入查询关键词-github,会根据浏览器默认搜索引擎,合成带查询关键词的URL。
https://www.baidu.com/s?ie=UTF-8&wd=github

如果用户输入的是符合URL规则的内容,则浏览器地址栏会尝试根据URL规则,合成完整的URL

1
2
3
4
5
6
# 输入符合URL规则的内容-github.com
# 由于缺少协议头,则浏览器会尝试为它补全协议头,合成为完整的URL
https://github.com
# 输入符合URL规则的内容-https://www.qq.com/
# 由于它本身便是完整的URL,浏览器不会做任何URL加工
https://www.qq.com/

判断beforeunload事件

在当前页面被替换为新的页面之前,会判断页面有没有beforeunload事件,beforeunload事件常用于完成以下的工作:

  • 页面退出之前的一些数据清理
  • 询问用户是否离开当前页面
  • 取消导航,不执行后续导航流程
1
2
3
4
5
6
window.addEventListener('beforeunload', (event) => {
// 根据规范,要显示确认,事件处理程序需要在事件上调用preventDefault()
event.preventDefault();
// 但是Chrome并没有遵守这个规定,Chrome要求设置returnValue
event.returnValue = '';
});

如果页面没有监听到beforeunload事件,或者事件已经同意进行后续的导航流程,则浏览器便进入加载状态(浏览器图标进入加载状态),进行后续导航流程。

浏览器进入加载状态

发起 URL 请求

在网络进程中发起真正的 URL 请求。

如果浏览器本地缓存了这个资源,则会直接将数据转发给浏览器进程,否则网络进程会接收并解析响应头数据,才会把数据转发给浏览器进程。

URL转发给网络进程

当完成了上述的工作,浏览器进程会通过**IPC(进程间通信)**把URL请求转发给网络进程,网络进程在接收到请求后,会发起真正的URL请求流程。

查找本地资源缓存

网络进程会先查找本地是否缓存了这个资源,如果有缓存,则直接返回资源给浏览器进程。

进入网络请求流程

如果没有查找到缓存资源,则进入网络请求流程。

DNS解析

根据URL查询对应的DNS缓存是否存在,且在有效期内,符合则直接返回缓存存储的服务器IP地址。

如果没有符合的DNS缓存,则会经过DNS服务器解析,得到服务器的IP地址,如果是HTTPS协议,还需要建立TLS连接。

建立TCP连接

利用IP地址和端口号(HTTP默认80端口,HTTPS默认443端口)建立TCP连接(同一域名同时最多只能建立6个TCP连接,超出连接数的请求需排队等待),随后浏览器会构建请求行,请求头(包含Cookie等数据),请求体等信息,向服务器发送请求信息。

服务器接收到信息后,会根据请求信息生成响应行、响应头和响应体等响应信息,并发送给网络进程。

网络进程接收到这些信息后,就开始解析响应信息了。

根据状态码处理请求

如果服务器返回的响应头状态码为301或者302

  • 301: 永久重定向,一般用于重定向被移除的资源
  • 302: 暂时重定向,用于临时重定向

则会根据响应头的Location字段,读取新的被重定向后的URL地址,再发起新的 HTTP 或者 HTTPS 请求,此时一切流程又重新开始了。

如果服务器返回的响应头状态码为200(正常响应),则会根据**服务器返回的响应体数据类型(Content-Type)**,决定如何显示响应体的内容。

1
2
3
4
# 告诉浏览器返回的是HTML格式的数据
Content-Type: text/html
# 告诉浏览器返回的数据是字节流类型,通常浏览器会安装下载类型来处理
Content-Type: application/octet-stream

如果是下载类型的数据,浏览器会把它提交给浏览器的下载管理器,此时,这个导航流程也结束了。

如果是HTML,则浏览器会继续进行导航流程。

准备渲染进程

默认情况下,Chrome会为每个页面分配一个渲染进程(每打开一个新页面就会配套创建一个新的渲染进程)。

但是在同一站点的情况下,多个页面会复用同一个渲染进程,Chrome官方把这个默认策略叫 process-per-site-instance。

同一站点(same-site)

满足以下所有条件,则表示为统一站点

  • 根域名相同
  • 协议相同

同一域名的条件只判断根域名和协议,所以,同一站点的定义还包含了该根域名下的所有子域名和不同端口。

1
2
3
4
# 这些情况都属于同一个站点
https://www.github.com
https://api.github.com
https://www.github.com:8081

提交文档

  • 当浏览器进程接收到网络进程的响应头数据后,便向渲染进程发起”提交文档”的信息。
  • 渲染进程接收到”提交文档”的信息后,会和网络进程建立数据传输的”管道”。
  • 等待文档数据传输完成后,渲染进程会返回”确认提交”的信息给浏览器进程。
  • 浏览器进程接收到”确认提交”的消息后,便开始移除之前旧的文档,然后更新浏览器的界面状态(包括了安全状态、地址栏的 URL、前进后退的历史状态),并更新页面(从此前 URL 网络请求时的逆时针旋转,即将变成顺时针旋转)。

到这里,一个完整的导航流程就完成了,这之后就要进入渲染阶段了。

渲染流程(渲染流水线)

一旦文档进入“确认提交”的状态,渲染进程便开始页面解析和子资源加载。

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

构建DOM树(DOM Tree)

因为浏览器无法理解和使用HTML,所以需要把HTML转换为浏览器能够理解的结构,这种结构就叫DOM树。

1
2
// 通过document可以查看DOM树结构
document

样式计算(Recalculate Style)

样式计算是为了计算DOM节点每个元素的具体样式,它分为3个步骤完成。

  1. 把CSS转换为浏览器能够理解的结构

    和HTML文件一样,浏览器也无法直接理解这些纯文本的CSS,这里会把CSS样式的三大来源(link标签引用的外部CSS文件,style标签内的CSS,元素style属性内嵌的CSS),转换为浏览器可以理解的结构——styleSheets。

    styleSheets这部分,也有个另外的称呼,叫做CSSOM,学习李兵大佬这个部分的时候,李兵大佬表示浏览器源码中并没有CSSOM这个称呼,也许这个称呼是约定俗称的说法?不过为了统一,后续都将使用styleSheets这个称呼,而非CSSOM。

    1
    2
    // 通过document.styleSheets,可以查看styleSheets(也称CSSOM)的结构
    document.styleSheets

    styleSheets中包含了上面CSS三大来源的样式,并且该结构同时具备了查询和修改功能,这会为后面的样式操作提供基础。

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

    作为前端的同学,应该都知道,CSS文本中有很多不同的属性值,比如代表字号的em,代表颜色的blue,bold,这些类型数值也不容易被渲染引擎理解,所以这一步,渲染引擎需要把所有值转换为渲染引擎容易理解,标准化的计算值,也叫属性值标准化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /* 原CSS属性值 */
    body { font-size: 2em; }
    p {color: blue;}
    span {display: none;}
    div {font-weight: bold;}
    div p {color: green;}
    div {color:red;}
    /* 标准化过程后的CSS属性值 */
    body { font-size: 32px }
    p {color: rgb(0,0,255)}
    span {display: none}
    div {font-weight: 700}
    div p {color: rgb(0,128,0)}
    div {color: rgb(255,0,0)}
  3. 计算出DOM树中每个节点的具体样式

    样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。

    CSS层叠和继承

    计算后 DOM 的样式-图源自李兵的浏览器工作原理与实践

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

布局阶段

有了DOM树和DOM树中的样式,接下来便是需要计算出DOM树中可见元素的几何位置,也叫布局。

  1. 创建布局树

    布局树是一颗只包含所有可见节点的树,所以像head标签,或者使用了display:none属性的元素,不能被包含到布局树中。

    布局树构造过程示意图-图源自李兵的浏览器工作原理与实践

  2. 布局计算

    有了布局树,就可以计算布局树节点的几何坐标位置了。

    执行布局操作的时候,会把布局的几何运算结果重新写回布局树中,所以布局树既是输入内容也是输出内容。(这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来)

    针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。

分层

由于页面有许多复杂的效果,如3D变换、页面滚动,或使用z-index做的z轴排序,所以渲染引擎还需要为这些特定的节点生成专用图层,并生成一颗对应的图层树(LayerTree)。它跟PhotoShop中图层的概念非常类似。

在Chrome开发者工具选择”Layers”标签,可以以可视化的方式查看页面的分层情况。

页面分层结构

通常情况下,并不是布局树每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就属于父节点的图层。

当满足以下两个条件之一的元素,渲染引擎就会为这个元素提升为单独的一个图层。

  1. 拥有层叠上下文属性的元素

    具体满足层叠上下文的规则可阅读下方的MDN文档,像明确的定位属性、透明属性、CSS 滤镜、z-index 等,都满足层叠上下文的规则。

    层叠上下文

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

    如使用overflow属性,当内容溢出盒子区域,出现被裁剪的情况,渲染引擎会为文字部分和滚动条(如果有)单独创建一个图层。

绘制列表(由绘制指令组成)

完成图层树的构建后,渲染引擎会对图层树中的每个图层进行绘制,绘制过程跟canvas很类似。

它会根据绘制每个图层的步骤,把一个图层绘制拆分成很多小的绘制指令(跟canvas的绘制指令非常类似),最后把这些指令按顺序组成一个待绘制列表。

可以通过Chrome开发者工具的”Layers”标签,选择”doucment”层,查看详细的绘制列表。

图层绘制

分块

绘制列表只是用来记录绘制顺序和绘制指令的列表,实际绘制操作是由渲染引擎的合成线程来完成。

当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。

然后合成线程会按照视口(屏幕上页面的可见区域就叫视口-ViewPort)附近的图块来生成位图,生成位图的操作是交由渲染进程内的栅格化进程池进行的。

栅格化(raster)

图块是栅格化执行的最小单位,所谓栅格化,是指将图块转换为位图。

渲染进程内有栅格化进程池,会优先拿到视口(屏幕上页面的可见区域就叫视口-ViewPort)附近的图块进行栅格化过程,栅格化过程都会使用GPU进行加速,栅格化后生成的位图被保存在GPU的内存中。

合成和绘制

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

到此, 从输入URL到页面展示的过程就全部完成了。

相关概念

从上面的渲染流水线,可以了解到,构建 DOM 树、样式计算、布局阶段和分层,都是在浏览器的主线程执行的。

而绘制列表、分块、光栅化和合成绘制,则是在浏览器的其他线程执行的。

更新了元素的几何属性(重排)

如果通过JavaScript或CSS修改了元素的几何位置属性,如改变元素的宽、高,就会触发浏览器的重新布局,重排需要重走一遍完整的渲染流水线,所以开销也是最大的。

更新元素的绘制属性(重绘)

通过JavaScript更改某些元素的背景颜色,因为几何位置属性没有改变,所以布局阶段将部分被执行,重绘会从渲染流水线的绘制阶段开始执行(也就是从绘制列表阶段开始),相较于重排操作,重绘省去了布局(构建布局树阶段)和分层阶段,所以执行效率会比重排操作要高一些。

直接合成阶段(合成)

如果只是更改一个即不要布局也不要绘制的属性,例如CSS的transform来实现动画,渲染引擎将跳过布局和绘制(也就是从分块开始),只执行后续的合成(从分块开始到最后的流水线称为合成子阶段)操作,我们把这个过程叫做合成。这样的效率是最高的,由于是在非主线程上合成,没有占用主线程的资源的同时,也避开了布局(构建布局树阶段)、分层和绘制(绘制列表称为绘制子阶段)三个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

总结

导航流程

  • 用户输入

    1. 用户在地址栏按下回车,检查输入(查询关键字 或 符合 URL 规则),组装完整 URL。
    2. 回车前,当前页面执行 onbeforeunload 事件。
    3. 浏览器进入加载状态。
  • URL 请求

    1. 浏览器进程通过 IPC(进程间通信) 把 URL 请求发送至网络进程。
    2. 查找页面资源缓存(有效期内),存在缓存且在有效期内,则直接返回给浏览器进程。
    3. 如果没有页面资源缓存,则根据URL地址查询DNS 解析(DNS缓存/DNS服务器),获得服务器的IP地址;
    4. 创建 TCP 连接(三次握手),单域名连接数最多6个,超出部分进入 TCP 队列,HTTPS还需要建立TLS连接。
    5. 发送 HTTP 请求(请求行[方法、URL、协议]、请求头[Cookie等]、请求体);
    6. 接受请求信息(响应行[协议、状态码、状态消息]、响应头、响应体等);
    • 状态码 301 / 302,触发重定向规则,根据响应头中的 Location 重定向;
    • 状态码 200,正常响应,根据响应头中的 Content-Type 决定如何响应(下载文件、加载资源、渲染 HTML)。
  • 准备渲染进程

    ​ 根据是否同一站点(相同的协议和根域名),决定是否复用渲染进程。

  • 提交文档

    1. 浏览器进程接受到网路进程的响应头数据,向渲染进程发送『提交文档』消息;
    2. 渲染进程收到『提交文档』消息后,与网络进程建立传输数据『管道』;
    3. 传输完成后,渲染进程返回『确认提交』消息给浏览器进程;
    4. 浏览器接受『确认提交』消息后,移除旧文档、更新界面、地址栏,导航历史状态等,此时标识浏览器加载状态的小圆圈,从此前 URL 网络请求时的逆时针旋转,即将变成顺时针旋转(进入渲染阶段)。

渲染(渲染流水线)

  • 构建 DOM 树

    ​ 把HTML使用解析器转换为浏览器能够理解的DOM结构。

  • 样式计算

    1. 把CSS转换为浏览器能够理解的结构-styleSheets(也称CSSOM)。
    2. 转换样式表中的属性值,使其标准化。
    3. 计算出DOM树中每个节点的具体样式(继承、层叠),并保存在 ComputedStyle 的结构内。
  • 布局阶段(DOM 树中元素的几何位置)

    1. 使用DOM & ComputedStyle构建成布局树(DOM 树中的可见元素)。
    2. 布局计算。
  • 分层

    1. 特定节点生成专用图层,生成一棵图层树(层叠上下文、Clip,类似 PhotoShop 里的图层)。
    2. 拥有层叠上下文属性(明确定位属性、透明属性、CSS 滤镜、z-index 等)的元素会创建单独图层。
    3. 需要剪裁的地方也会创建图层。
    4. 没有图层的 DOM 节点属于父节点图层。
  • 绘制列表(由绘制指令组成)

    1. 输入:图层树;
    2. 渲染引擎对图层树中每个图层进行绘制;
    3. 拆分成绘制指令,生成绘制列表,提交到合成线程;
    4. 输出:绘制列表。
  • 分块

    合成线程会将较大、较长的图层(一屏显示不完,大部分不在视口内)划分为图块(tile, 256*256, 512*512)。

  • 光栅化(栅格化)

    1. 在光栅化线程池中,将视口附近的图块优先生成位图(栅格化执行该操作);
    2. 快速栅格化:GPU 加速,生成位图(GPU 进程)。
  • 合成绘制

    1. 绘制图块命令——DrawQuad,提交给浏览器进程;
    2. 浏览器进程的 viz 组件,根据DrawQuad命令,绘制在屏幕上。

相关概念

  • 重排

    1. 更新了元素的几何属性(如宽、高、边距);
    2. 触发重新布局,解析之后的一系列子阶段;
    3. 更新完成的渲染流水线,开销最大。
  • 重绘

    1. 更新元素的绘制属性(元素的颜色、背景色、边框等);
    2. 布局阶段不会执行(无几何位置变换),直接进入绘制阶段。
  • 合成

    1. 直接进入合成阶段(例如CSS 的 transform 动画);
    2. 直接执行合成阶段,开销最小。

从输入URL到页面展示,这中间发生了什么?
https://sothx.com/2021/06/14/urlToPage/
作者
Sothx
发布于
2021年6月14日
许可协议