[TOC]

broswer_theory.jpg

要说清楚从浏览器输入一串URL输入到界面展示的过程是一个并不容易的工作。这个问题确实可以全面考察一个人的计算机知识,以及对它们的熟悉深度。如果细说这些理论,完全可以编辑成一本书;而如果说简单的说,也可以寥寥数语就说完。我们这篇博文总体是从一名前端工程师的角度来看待这件事情的。从计算机基础,到网络传输再到浏览器的渲染原理,每一部分涉及的深度和广度都会不一样。例如:我们会更少地涉及计算机基础,而更多地关注浏览器渲染原理,因为后者才是前端工程师的主战场;在介绍网络篇的时候我们也只是列出了网络包体发送的几个耗时间的关键步骤,如果要深究细节,一篇博文是远远不够的。最后,对底层原理越是深入了解,在实际应用中就越能透过现象看到问题的本质,并且最终解决它。

系统阶段

为了了解浏览器是如何讲用户的输入行为转化称应用界面的,我们需要向下看一点,回到我们学习过的计算机基础层面,了解这些概念,有助于从宏观视角上去理解浏览器是如何工作的。

CPU、进程

cpu是电脑的最核心部件。它处理了每一个电脑的动作上的每一条信息。一个cpu就是一台有效能的机器人,你在电脑上的所有操作最终都会被cpu处理。早期的电脑只有一个cpu,随着电子科学以及芯片制造技术的进步,现在一台电脑都拥有多个cpu,我们平时称拥有多个cpu的电脑为多核电脑,多核意味着处理性能的提升。cpu属于计算机的底层硬件设置,在这之是是操作系统,操作系统是应用程序的底层执行平台。
你打开某个应用,电脑就会为你分配一块内存地址,并且开启一系列的进程为你这个进程服务,每个进程之后都可以通信,我们称这种通信方式为IPC**(**Internal Processing Communication)。当应用关闭后,内存被释放,进程也随之消失。
16.png

浏览器的多进程架构

浏览器与其他应用一样,启动的时候系统会为他分配一块内存,启动相关的进程,以及GPU服务。一般来说,浏览器启动一个标签的时候打开了至少以下几个进程:

  1. 浏览器进程(Broswer Process):浏览器进程是多个服务的合集。chrome为了权衡服务与内存分配,把多个进程合并到了该进程之中。例如UI进程,设备进程,本地存储以及网络进程。它的主要责任是负责浏览器的UI控制,侦听各种事件响应:如窗口的放大缩小,滚动条的滚动,手势的判断,input的组件的输入以及其他用户交互行为,然后将这些行为通过ipc传递给渲染进程进行处理。
  2. GPU进程(GPU Process):GPU作为图形输出的重要进程,参与到各个应用当中,不仅仅是浏览器应用,其他需要图形服务的应用都会启用该进程。
  3. 渲染进程(Render Process):渲染进程就是主要负责界面的进程。这个进程上运行的v8引擎解析和编译javascrit,负责生成dom的解析器以及以及css解析器生成CSSOM。渲染进程能同时运行多条线程,但是大多数的交互和显示工作是在主线程上完成的。而主线程采用的是事件循环机制,单线程处理各种宏任务和微任务。
  4. 插件进程(Plugin Process):插件进程在运行过程中主要运行的是浏览器的各种插件。插件进程是个沙箱,它与其他的进程是完全隔离的。因为第三方插件很有可能危害用户的信息安全。

17.png
早期的浏览器是单进程的,这是因为那时的电脑大多数是单核浏览器,用户的电脑难以支持多经常的分配操作,所有的进程都合二为一,插件,渲染,网络所有的功能模块都挤在一起,这样就导致了糟糕的用户体验——某个标签卡死了,整个浏览器都奔溃了。单进程的另外一个缺点就是不安全,插件脚本容易被第三方的恶意代码注入html,造成用户的隐私泄漏。2008年11月发布的chrome浏览器采用了多进程的架构,浏览器为每个标签都开辟了一个不同的不同的进程,这些进程互不干扰,彼此为自己的标签页提供相关服务。

渲染进程的分配规则

渲染进程理论上是对每个标签页面进行分配的,这个我们在上文中已经提到。Chrome在版本64中引入了站点隔离技术(site isolation),为每一个iframe也启用一个渲染进程,这样就再次将隔离站点细化。但是每分配一个渲染进程,意味着需要多消耗cpu以及内存,在标签页面过多的时候就会出现内存或者cpu的占用率飙升的情况。不管是window转到mac os系统,chrome浏览器确实是吃掉内存的罪魁祸首。针对内存消耗的问题,浏览器需要对这种固化的原则进行灵活处理。下面我们就来看看chrome团队如何去在性能和体验上作出权衡的。首先我们明确一下两个概念:
浏览器中上下文(Browsing contexts)

A browsing context is an environment in which Document objects are presented to the user. 浏览器上下文指呈现给用户的文档对象所处的环境。例如window对象,历史记录,导航,滚动等文档对象。这些文档对象就被包含在浏览器的上下文中。

浏览器中上下文组(Groupings of browsing contexts)

A browsing context group holds a browsing context set (a set of top-level browsing contexts). 浏览器上下文组,是指一系列文档对象环境的集合。例如从一个界面当中通过标签或者脚本打开的下一个标签,这些标签共同组成一个上下文组。

同一浏览器上下文组打开的同一站点,多个标签会共用一个渲染进程

  1. 不同的上下文组或者不同的站点,浏览器会开辟新的渲染进程。
  2. a标签设置了rel=noopener属性时,通过该标签访问的新标签,会被渲染新的进程。
  3. 标签页和iframe如果是同一站点,也共用同一渲染进程。

以上便是一些渲染进程分配的细节和原则。你可以打开自己的浏览器,通过task面板查看相关的信息。chrome的进程分配使得我们的浏览器更加安全和有效。但这样做也是有代价的:你的电脑cpu和内存消耗会徒然上升,电脑的温度直线飙升。如果你是chrome的用户,我想你一定经历过这样的事情:买电脑,送暖手宝。

开始输入地址

现在,在打开浏览器之后,我们可以开始输入地址了,首先登场的是浏览器进程。万维网的地址为了便于记忆,一般采用单词或者容易记住的字母组合,例如baidu.com|google.com。而服务器IP地址则是按照一定的规则排列的数字组合。所以当用户按下回车键,浏览器进程会首先会拿用户的输入去DNS服务器查找对于的ip地址,同时更新浏览器历史堆栈。浏览器的UI上这时也有变化,一个是亮起的回退键,另外一个是标签页左上角开始转圈圈。

网络阶段

传播速度

科学告诉我们光在真空中的传播速度大约为30万千米每秒,但在其他介质中的速度并没有在真空中那么快。例如光在水中或者在其他介质当中传播的速度就会明显的下降。现在的多数网线采用了光学纤维替代了铜线,而电子信号在这些光学纤维中传播速度大概只有光速的2/3,也就是20万千米每秒。听起来也是很快,但考虑到实际的物理距离,电子信号从相隔较远的亮点之前做一个来回所产生的时间,足够造成用户体验上的损失。例如,中国距离美国大概一万四千公里,数据在客户端到服务端的一个来回(RTT),大概是140ms。140ms听起来不算太久,但是考虑到TCP世界中的游戏规则,我们总共花的时间可能要多少好几倍。

TCP建立链接

在收到浏览器的通知,网络进程便开始去与服务器交互了。网络进程属于浏览器进程中的一部分,负责建立TCP链接,发送和接受网络请求并且与将收到的数据发给渲染进程进行处理。首先网络进程驱动操作系统,建立tcp链接,两台机器建立链接需要相互确认动作,它们会给彼此发送序列号,来表示自己的身份,也就是我们经常听到的三次握手:

  • 客户端生成一个随机序列(SYN) 给服务器。
  • 服务器拿到随机数列,在此随机数上加1,返回另外一个之前的随机数SYN,并且也生成随机数列ACK
  • 客户端收到SYN确认和并且同时确认ACK加一,发送给服务器端,服务器验证了客户端便开始请求后立即发送请求信息。

TCP的建立一共需要三次握手,就是1.5个RTT,如果考虑到如果使用了TSL协议,那么总数达到 2.5RTT。前面我们讲过,不同区域网络传输时长会因为客户端与服务器之间的物理距离而不同,如果物理距离过大,这个时间经过倍数叠加就会变得很长,所以我们常说建立tcp链接是昂贵的。在http1.0的时代,一个域名只允许建立一个tcp链接,并且在http传输完成之后自动关闭tcp链接,客户端如果同时出现很多请求,会被浏览器排队。你可以通过network面板中看到这些信息被阻塞的时长和状态。这种低效的传输模式显然无法支撑日益增长的互联网用户的需求。http急需改良性能。现代浏览器一般同时允许建立4~8个tcp链接,并且保持长久开启(keep alive)的状态,以便继续发送http请求。这暂时缓解了三次握手以及多请求阻塞的问题。但显然,这些都还只是权宜之计。
14.png

TCP传输过程

  • 拥塞窗口

TCP传输的数据并不是一次性全部传输完成的,这些信息会被切分成一个个的小的数据包,分批次传输。为了防止一次性大量发送数据从而导致收发端无法处理,服务器和客户端都对发送的数据包大小做了限制:给定一个初始数据发送大小的值,每次发送数据量都不超过窗口值,窗口值随着rtt的次数增加(在前一个窗口的值上翻倍)。这个限制数据量大小的窗口,我们称之为拥塞窗口**Congestion window (rwnd)。而这种处理数据流的发送方式的行为,我们称之为TCP的慢启动(slow start)**
15.png

  • 拥塞避免

TCP在传输过程中有可能出现丢包的情况,为了保障数据的完整性,tcp采用个两种办法避免因为丢包而造成的时间损失。第一种是快速重传(fast retransmit),快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期需要注意的是,重传的起始窗口不再使用初始拥塞窗口值,而是回归在上一次窗口值。这种做称之为快速恢复(fast recovery)
13.png

  • 队头阻塞

我们之前说过tcp传输数据是分片的,并且会出现丢包,重传,慢启动。一个TCP通道只负责传输一个http,这些http就串行起来,在等待重传的过程中,造成了其他后续的包的时间阻塞,网络带宽也被白白浪费了。这就是tcp的对头阻塞。对头阻塞总得来说是http管道技术的不成熟,以及tcp为了保证成的数据包发送策略的结果。对头阻塞对http影响非常大,不过这个问题在http2中被解决了。

TCP小结

http发送之前确实需要做很多事情。tcp就是在为这些事情做准备。总的来说,它花费在握手,慢启动,拥塞窗口算法控制。续后我们会出一篇博文对其进行优化,本篇值对这些影响时间的因素进行说明,无论如何,每个http请求都需要走以便上面的过程。同时,chrome的network面板中还有这些时间的详。关于tcp的数据传输我们完全可以另外开一篇博文甚至是一本书来讲解。不过本篇仅仅把它当作网页加载的一个阶段进行简单的说明。

服务器阶段

动静分离

请求到服务器后一般都是由方向代理服务器负责处理请求分发的。一般来说服务器上都有http的代理服务器,例如我们比较熟悉的Nginx,Apache。这些代理服务器将80端口代理成html的静态资源路径,当我们请求发过来的时候,资源文件会被直接作为字节流返回到浏览器中。

服务器脚本

凡事都有由例外,如今很多前端架构,工程师经常需要动态生成html(如SSR架构的前端网页应用),这种时候html就并非直接由静态服务器直接放回,而是需要经过后台语言的插入数据,生成模板,解析转化html标签等操作生成的。这种操作能够动态生成html,能有效减少因为依赖脚本而界面的时间,所以在前端架构中经常被用到。不过,因为需要经过编程语言,数据查询,异步I/O等处理,这种效率是远远低于前一种方式的。不管采用哪一种方式,我们最终生成的是html格式的文件,这样浏览器才能继续执行我们下面的操作。当然也可以是其他文本类型。不过我们这篇博文主要讲的是html的界面渲染过程,因此暂时忽略其他格式的静态资源。请求内容准备好了之后,服务器便开始讲这些内容返回到客户端了进行下载了。

TTFB

服务器收到了TCP请求,经过了一系列动作,例如:解析参数,分析地址,执行代码,读写文件或者数据库操作等,最终将一段头信息以及内容返回给客户端。从请求发出,到客户端收到服务器发回的第一个字节信息所经历的时间,我们称之为TTFB(Time To First Byte)。TTFB受网络条件和服务器架构影响,在资源体积较小的情况下通常会占有网络过程的大部分时间,TTFB同时是衡量网页性能的一个重要指标,你可以在chrome浏览器的network中查看每个资源的TTFB时间。
25.png

浏览器阶段

接收信息

我们的请求经过万水千山的跋涉,终于回到了浏览器的中。网络进程接收到第一个字节便交给浏览器渲染进程。通常,为了减少包的体积,服务器通常会对资源进行压缩处理。浏览器进程根据服务器返回的压缩包的方式进行对应的解压工作。解压后的信息渲染进程就可以开始处理了。

渲染进程

网络进程收到了第一个字节后,通过ipc通信给渲染进程。渲染进程便开始对dom进行解析。解析的动作是和下载的动作同时进行的,字节一点点的下载下来,同时也被一点点的被渲染引擎解析。dom通过文档结构告知浏览器开始序列化,token等操作生成一棵dom树,这棵数的根节点是html,接下来一点点开枝散叶。

除了字节下载的速度,在渲染过程中还有很多因素制约着DOM文档结构的生成以及绘制。这其中最主要的就是css和javscript文件的下载和执行。Javascript和Css都会影响Dom的解析和渲染,我们后面将会详细地介绍到。其他资源,如视频,图片都会随着dom的解析被下载到浏览器中,但它们都不影响html的解析。我们将css和js这些能影响到界面渲染绘制的资源称之为关键资源(Critical Resources),而这些关键资源的个数和,我们称之为CRP(Critical Rendering Path),通过判断关键资源路径(CRP)可以从宏观的角度计算我们的网络加载速度。

需要注意的是,并非所有js和css都算关键资源,例如添加了defer或者async熟悉的脚本或者根据媒体样式进行分类加载查询的层叠样式表。

现在,我们来对渲染的过程进行稍微深入一点的讲解。大致来说,我们在渲染的过程主要干了以下几件事情:

1.解析dom
将返回的html文本解析成dom树是浏览器的首要任务。随着网络进程一点点的把字节信息返回给渲染浏览器,dom结构便开始一点点的生成。html语法结构宽松,对大小写以及不规范的书写方式较为友好和兼容,在生成dom的时候有些元素比如head或者display:none的元素不会被纳入dom树。渲染引擎解析dom的速度非常非常快,以至于很多时候都必须等待字节被下载下来,为了尽快的渲染出界面,渲染引擎会把首先下载并且渲染过的界面部分直接绘制到屏幕上。等待内容被下载下载,再继续解析和绘制下面的元素。你可以把网速调低一些,打开google,看看它的界面是如何呈现在你的眼前的。dom被完全解析后会触发DomContentLoaded事件。
23.png

2.子资源的下载
早期的html,只有纯文本。而且文字是随着html文档一起被下载下来的。随着web应用的发展,网页应用出现了图片,视频,脚本,样式表等资源文件用来丰富自身的体验。因此,html早不再是一个单份的文本文档。通过特殊的标签(渲染 - 图9