浏览器工作原理学习总结

总结前说明本文针对浏览器工作原理学习总结都是基于 Chrome 浏览器的。那么多浏览器,为什么偏偏选择 Chrome 浏览器?

因为 Chrome、微软的 Edge 以及国内的大部分主流浏览器,都是基于 Chromium 二次开发而来;而 Chrome 是 Google 的官方发行版,特性和 Chromium 基本一样;再加上 Chrome 是目前世界上使用率最高的浏览器,所以 Chrome 最具代表性。

浏览器架构

Chrome 打开一个页面需要启动多少进程?答案是至少4个。

为什么仅仅打开了 1 个页面,为什么有 4 个进程?因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。截图里多了Audio和Storage服务进程。

最初的浏览器都是单进程的,它们不稳定、不流畅且不安全,之后出现了 Chrome,引入了多进程架构,并解决了这些遗留问题。

另外对进程和线程感兴趣可以查看:线程与进程,你真的理解了吗

TCP/IP协议

互联网,实际上是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。

不管是使用 HTTP,还是使用 WebSocket,它们都是基于 TCP/IP 的。

IP把数据包送达目的主机

数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准。互联网上不同的在线设备都有唯一的地址,地址只是一个数字,这和大部分家庭收件地址类似,只需要知道一个家庭的具体地址,就可以往这个地址发送包裹,这样物流系统就能把物品送到目的地。

计算机的地址就称为 IP 地址,访问任何网站实际上只是计算机向另外一台计算机请求信息。

UDP把数据包送达应用程序

IP 是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给哪个程序,是交给浏览器还是交给微信?因此,需要基于 IP 之上开发能和应用打交道的协议,最常见的是"用户数据包协议(User Datagram Protocol)",简称 UDP。

UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号,UDP 就能把指定的数据包发送给指定的程序了,所以 IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。

和 IP 头一样,端口号会被装进 UDP 头里面,UDP 头再和原始数据包合并组成新的 UDP 数据包。UDP 头中除了目的端口,还有源端口号等信息。

TCP把数据完整地送达应用程序

对于浏览器请求,或者邮件这类要求数据传输可靠性(reliability)的应用,如果使用 UDP 来传输会存在两个问题

  1. 数据包在传输过程中容易丢失;
  2. 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。

毕竟 UDP 负责的是把数据包送达应用程序,对完整性并没有过多保证,基于这两个问题,引入 TCP。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

相对于 UDP,TCP 有下面两个特点

  1. 对于数据包丢失的情况,TCP 提供重传机制;
  2. TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。和 UDP 头一样,TCP 头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。

HTTP请求流程

浏览器端发起 HTTP 请求流程

如果在浏览器地址栏里键入https://www.baidu.com/,浏览器会完成哪些动作呢?下面就一步一步详细"追踪"下。

构建请求

首先,浏览器构建请求信息,构建好后,浏览器准备发起网络请求。

查找缓存

在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。

其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。

当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。

这样做的好处有:

  • 缓解服务器端压力,提升性能(获取资源的耗时更短了)
  • 对于网站来说,缓存是实现快速资源加载的重要组成部分。当然,如果缓存查找失败,就会进入网络请求过程了
准备 IP 地址和端口

前面介绍过数据包都是通过 IP 地址传输给接收方的。由于 IP 地址是数字标识,比如百度网站的 IP 是 110.242.68.4:443, 难以记忆,但使用百度的域名(baidu.com)就好记多了,所以基于这个需求又出现了一个服务,负责把域名和 IP 地址做一一映射关系。这套域名映射为 IP 的系统就叫做"域名系统",简称 DNS(Domain Name System)。

所以浏览器请求 DNS 返回域名对应的 IP。当然浏览器还提供了 DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。

拿到 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。

等待 TCP 队列

现在已经把端口和 IP 地址都准备好了,那么下一步是不是可以建立 TCP 连接了呢?答案依然是"不行"。

Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。

当然,如果当前请求数量少于 6,会直接进入下一步,建立 TCP 连接。

建立 TCP 连接

排队等待结束之后,终于可以快乐地和服务器握手了。在 HTTP 工作开始之前,浏览器通过 TCP 与服务器建立连接。

发送 HTTP 请求

一旦建立了 TCP 连接,浏览器就可以和服务器进行通信了。而 HTTP 中的数据正是在这个通信过程中传输的。

首先浏览器会向服务器发送请求行,它包括了请求方法、请求 URI(Uniform Resource Identifier)和 HTTP 版本协议。

发送请求行,就是告诉服务器浏览器需要什么资源,最常用的请求方法是 Get。比如,直接在浏览器地址栏键入百度的域名(baidu.com),这就是告诉服务器要 Get 它的首页资源。

另外一个常用的请求方法是 POST,它用于发送一些数据给服务器,比如登录一个网站,就需要通过 POST 方法把用户信息发送给服务器。

如果使用 POST 方法,那么浏览器还要准备数据给服务器,这里准备的数据是通过请求体来发送。在浏览器发送请求行命令之后,还要以请求头形式发送其他一些信息,把浏览器的一些基础信息告诉服务器。

比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的 Cookie 信息等等。

服务器端处理 HTTP 请求流程

接下来,服务器会根据浏览器的请求信息来准备相应的内容,做出相应的反应。

返回请求

首先服务器会返回响应行,包括协议版本和状态码。

但并不是所有的请求都可以被服务器处理的,那么一些无法处理或者处理出错的信息,怎么办呢?服务器会通过请求行的状态码来告诉浏览器它的处理结果,

比如:最常用的状态码是 200,表示处理成功;如果没有找到页面,则会返回 404;如果服务端报错一般返回500等等。

随后,正如浏览器会随同请求发送请求头一样,服务器也会随同响应向浏览器发送响应头。响应头包含了服务器自身的一些信息,比如服务器生成返回数据的时间、返回的数据类型(JSON、HTML、流媒体等类型),以及服务器要在客户端保存的 Cookie 等信息。

发送完响应头后,服务器就可以继续发送响应体的数据,通常,响应体就包含了 HTML 的实际内容。

断开连接

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。

不过如果浏览器或者服务器在其头信息中加入了:Connection:Keep-Alive,那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。

保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个 Web 页面中内嵌的图片就都来自同一个 Web 站点,如果初始化了一个持久连接,就可以复用该连接,以请求其他资源,而不需要重新再建立新的 TCP 连接。

导航流程

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

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

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

渲染流程

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

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

构建 DOM 树

为什么要构建 DOM 树?这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构------DOM 树。

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

已经生成 DOM 树了,但是 DOM 节点的样式依然不知道,要让 DOM 节点拥有正确的样式,这就需要样式计算了。

样式计算(Recalculate Style)

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。

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

从图中可以看出,CSS 样式来源主要有三种:

  • 通过 link 引用的外部 CSS 文件
  • style标记内的 CSS
  • 元素的 style 属性内嵌的 CSS

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构------styleSheets。

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

要理解什么是属性值标准化,可以看下面这样一段 CSS 文本:

css 复制代码
body { font-size: 2em }
p {color:blue;}
span  {display: none}
div {font-weight: bold}
div  p {color:green;}
div {color:red; }

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。

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

样式具体计算阶段要考虑两点:CSS 继承和样式层叠。因为 CSS 具体样式默认会受到 CSS 继承和样式层叠的影响。

样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle 的结构内。

布局阶段

现在有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,把这个计算过程叫做布局。

创建布局树

为了构建布局树,浏览器大体上完成了下面这些工作:

遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如属性包含 dispaly:none 的标签。

布局计算

现在有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。

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

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

分层

现在有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?答案依然是否定的。

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

可以这样理解:布局是水平面的,而分层是垂直层的。

满足以下两点中的任意一点会被提升为单独的一层

  • 拥有层叠上下文属性的元素会被提升为单独的一层
  • 需要剪裁(clip)的地方也会被创建为图层

图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制,那么接下来我们看看渲染引擎是怎么实现图层绘制的?

试想一下,如果有一张纸,要求先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。该怎么操作呢?

通常绘制操作分解为三步:

  • 绘制蓝色背景
  • 在中间绘制一个红色的圆
  • 再在圆上绘制绿色三角形

渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

栅格化(raster)操作

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

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

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

合成和显示

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

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

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

总结

一个完整的渲染流程大致可总结为如下:

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构
  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式
  3. 创建布局树,并计算元素的布局信息
  4. 对布局树进行分层,并生成分层树
  5. 为每个图层生成绘制列表,并将其提交到合成线程
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图
  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程
  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上

(本文完)

相关推荐
吕彬-前端10 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱12 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai21 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb