宏观视角下的浏览器
- Chrome架构
- 线程 VS 进程
- 进程架构
- TCP协议
- IP:把数据包送达目的主机
- UDP:把数据包送达应用程序
- TCP:把数据完整地送达应用程序
- HTTP请求流程
- 构建请求
- 查找缓存
- 准备IP地址和端口
- 等待TCP队列
- 建立TCP连接
- 发送HTTP请求
- 服务器端处理HTTP请求流程
- 返回请求
- 断开连接
- 重定向
- HTTP请求示意图
- 从输入URL到页面展示完整
- 用户输入
- URL请求过程
- 准备渲染进程
- 提交文档
- 渲染阶段
- HTML、CSS和JavaScript,是如何变成页面的?
- 渲染流程
- 构建DOM树
- 样式计算(Recalculate Style)
- 布局阶段
- 渲染流程
- 分层
- 图层绘制
- 栅格化(raster)操作
- 合成和显示
- 相关介绍
Chrome架构
线程 VS 进程
多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。那什么又是进程呢?
一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
从图中可以看到,线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。
总结来说,进程和线程之间的关系有以下4个特点。
1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
2. 线程之间共享进程中的数据。
3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。
4. 进程之间的内容相互隔离。
进程架构
单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,
- 存在不稳定(插件来实现诸如Web视频、Web游戏等各种强大的功能)
- 不流畅(脚本或者插件会让单进程浏览器变卡顿外,页面的内存泄漏也是单进程变慢的一个重要原因)
- 不安全 (通过浏览器的漏洞来获取系统权限)
你可以先看看下面这张图,这是2008年Chrome发布时的进程架构。
我们先看看最新的Chrome进程架构,你可以参考下图:
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
未来面向服务的架构
为了解决这些问题,在2016年,Chrome官方团队使用“面向服务的架构”(Services Oriented Architecture,简称SOA)的思想设计了新的Chrome架构。也就是说 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展,原来的各种模块会被重构成独立的服务(Service),每个服务(Service)都可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,通过IPC来通信,从而构建一个更内聚、松耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。
TCP协议
在衡量Web页面性能的时候有一个重要的指标叫“FP(First Paint)”,是指从页面加载到首次开始绘制的时长。这个指标直接影响了用户的跳出率,更快的页面响应意味着更多的PV、更高的参与度,以及更高的转化率。那什么影响FP指标呢?其中一个重要的因素是网络加载速度。
互联网,实际上是一套理念和协议组成的体系架构。
那么如何保证页面文件能被完整地送达浏览器呢?
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来传输会存在两个问题:
- 数据包在传输过程中容易丢失;
- 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而UDP协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。
基于这两个问题,我们引入TCP了。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于UDP,TCP有下面两个特点:
- 对于数据包丢失的情况,TCP提供重传机制;
- TCP引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
和UDP头一样,TCP头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。
从下图可以看出,一个完整的TCP连接的生命周期包括了“建立连接”“传输数据”和“断开连接”三个阶段。
- 首先,建立连接阶段。这个阶段是通过“三次握手”来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作。所谓三次握手,是指在建立一个TCP连接时,客户端和服务器总共要发送三个数据包以确认连接的建立。
- 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照TCP头中的序号为其排序,从而保证组成完整的数据。
- 最后,断开连接阶段。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接。
到这里你应该就明白了,TCP为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
HTTP请求流程
HTTP是一种允许浏览器向服务器获取资源的协议,是Web的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如HTML文件、CSS文件、JavaScript文件、图片、视频等。此外,HTTP也是浏览器使用最广的协议,所以要想学好浏览器,就要先深入了解HTTP。
构建请求
首先,浏览器构建请求行信息(如下所示),构建好后,浏览器准备发起网络请求。
查找缓存
在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。
准备IP地址和端口
不过,先不急,在了解网络请求之前,我们需要先看看HTTP和TCP的关系。因为浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息;并使用TCP/IP作传输层协议将它发到网络上,所以在HTTP工作开始之前,浏览器需要通过TCP与服务器建立连接。也就是说HTTP的内容是通过TCP的传输数据阶段来实现的,你可以结合下图更好地理解这二者的关系。
这套域名映射为IP的系统就叫做“域名系统”,简称DNS(Domain Name System)。
所以,这样一路推导下来,你会发现在第一步浏览器会请求DNS返回域名对应的IP。当然浏览器还提供了DNS数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。
拿到IP之后,接下来就需要获取端口号了。通常情况下,如果URL没有特别指明端口号,那么HTTP协议默认是80端口。
等待TCP队列
现在已经把端口和IP地址都准备好了,那么下一步是不是可以建立TCP连接了呢?
答案依然是“不行”。Chrome有个机制,同一个域名同时最多只能建立6个TCP连接,如果在同一个域名下同时有10个请求发生,那么其中4个请求会进入排队等待状态,直至进行中的请求完成。
当然,如果当前请求数量少于6,会直接进入下一步,建立TCP连接。
建立TCP连接
发送HTTP请求
首先浏览器会向服务器发送请求行,它包括了请求方法、请求URI(Uniform Resource Identifier)和HTTP版本协议。
发送请求行,就是告诉服务器浏览器需要什么资源,最常用的请求方法是Get。比如,直接在浏览器地址栏键入极客时间的域名(time.geekbang.org),这就是告诉服务器要Get它的首页资源。
另外一个常用的请求方法是POST,它用于发送一些数据给服务器,比如登录一个网站,就需要通过POST方法把用户信息发送给服务器。如果使用POST方法,那么浏览器还要准备数据给服务器,这里准备的数据是通过请求体来发送。
服务器端处理HTTP请求流程
返回请求
断开连接
Connection:Keep-Alive
那么TCP连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求。保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。
重定向
到这里似乎请求流程快结束了,不过还有一种情况你需要了解下,比如当你在浏览器中打开geekbang.org后,你会发现最终打开的页面地址是 https://www.geekbang.org。
HTTP请求示意图
为了便于你理解,我画了下面这张详细的“HTTP请求示意图”,用来展现浏览器中的HTTP请求所经历的各个阶段。
从图中可以看到,浏览器中的HTTP请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备IP和端口、等待TCP队列、建立TCP连接、发起HTTP请求、服务器处理请求、服务器返回请求和断开连接。
从输入URL到页面展示完整
这其中,用户发出URL请求到页面开始解析的这个过程,就叫做导航。
用户输入
URL请求过程
从返回的响应头信息来看,其Content-Type的值是application/octet-stream,显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。
需要注意的是,如果服务器配置Content-Type不正确,比如将text/html类型配置成application/octet-stream类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成了一个下载文件。
所以,不同Content-Type的后续处理流程也截然不同。如果Content-Type字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该URL请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于Chrome的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。
准备渲染进程
默认情况下,Chrome会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。
那什么情况下多个页面会同时运行在一个渲染进程中呢?
要解决这个问题,我们就需要先了解下什么是同一站点(same-site)。具体地讲,我们将“同一站点”定义为根域名(例如,geekbang.org)加上协议(例如,https:// 或者http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:
https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080
它们都是属于同一站点,因为它们的协议都是HTTPS,而且根域名也都是geekbang.org。
Chrome的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫process-per-site-instance。
提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程,具体流程是这样的:
- 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。
渲染阶段
HTML、CSS和JavaScript,是如何变成页面的?
HTML的内容是由标记和文本组成。标记也称为标签,每个标签都有它自己的语义,浏览器会根据标签的语义来正确展示HTML内容。比如上面的
标签是告诉浏览器在这里的内容需要创建一个新段落,中间的文本就是段落中需要显示的内容。
如果需要改变HTML的字体颜色、大小等信息,就需要用到CSS。CSS又称为层叠样式表,是由选择器和属性组成,比如图中的p选择器,它会把HTML里面
标签的内容选择出来,然后再把选择器的属性值应用到
标签内容上。选择器里面有个color属性,它的值是red,这是告诉渲染引擎把
标签的内容显示为红色。
至于JavaScript(简称为JS),使用它可以使网页的内容“动”起来,比如上图中,可以通过JavaScript来修改CSS样式值,从而达到修改文本颜色的目的。
渲染流程
一个完整的渲染流程大致可总结为如下:
- 渲染进程将HTML内容转换为能够读懂的DOM树结构。
- 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令DrawQuad给浏览器进程。
- 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上
构建DOM树
为什么要构建DOM树呢?这是因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树。
样式计算(Recalculate Style)
样式计算的目的是为了计算出DOM节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
- 把CSS转换为浏览器能够理解的结构。当渲染引擎接收到CSS文本时,会执行一个转换操作,将CSS文本转换为浏览器可以理解的结构——styleSheets。
- 转换样式表中的属性值,使其标准化,需要将所有值转换为渲染引擎容易理解的、标准化的计算值,
- 计算出DOM树中每个节点的具体样式,这就涉及到CSS的继承规则和层叠规则了。
- 首先是CSS继承。CSS继承就是每个DOM节点都包含有父节点的样式。
- 层叠是CSS的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在CSS处于核心地位,CSS的全称“层叠样式表”正是强调了这一点。
布局阶段
现在,我们有DOM树和DOM树中元素的样式,但这还不足以显示页面,因为我们还不知道DOM元素的几何位置信息。那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。
- 创建布局树 。在显示之前,我们还要额外地构建一棵只包含可见元素布局树。
- 布局计算。现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了
从图中可以看出,本节内容我们介绍了渲染流程的前三个阶段:DOM生成、样式计算和布局。要点可大致总结为如下:
- 浏览器不能直接理解HTML数据,所以第一步需要将其转换为浏览器能够理解的DOM树结构;
- 生成DOM树后,还需要根据CSS样式表,来计算出DOM树所有节点的样式;
- 最后计算DOM元素的布局信息,使其都保存在布局树中。
渲染流程
分层
渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
- 拥有层叠上下文属性的元素会被提升为单独的一层。
- 第二点,需要剪裁(clip)的地方也会被创建为图层。
图层绘制
渲染引擎实现图层的绘制与之类似,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表
栅格化(raster)操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系:
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)合成线程会将图层划分为图块(tile)然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
相关介绍
如果你通过JavaScript或者CSS修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。
如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。
我们使用了CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。