JavaScript

浏览器工作原理

字数:9143    阅读时间:46min
阅读量:169

前言

对于前端开发来说,熟悉浏览器的运行原理是必须要掌握的。本文将尝试从浏览器基本结构HTTP解析流程浏览器渲染流程,三个方面归纳总结浏览器的基本工作原理。在进行正文之前我们可以先了解一下,当我们在浏览器地址栏输入网站地址时,浏览器所经历的大致流程:

  1. DNS解析: 浏览器先从缓存里查找是否存在当前URL,如果没有进行DNS查找,先访问顶级域名服务器,将对应的ip发给客户端;然后访问根域名解析器,将对应的ip发给客户端;最后访问本地域名服务器,得到最终的ip地址.
  2. TCP连接:在找到服务器之后,浏览器会通过 TCP 握手机制跟服务器建立连接(三次握手)。
  3. 发送HTTP请求:客户端和服务端建立连接后,客户端发起 HTTP 或 HTTPS 请求。
  4. 接收HTTP响应数据:服务端收到客户端的请求,发送HTTP响应报文给客户端,客户端获取到页面静态资源。
  5. TCP断开连接:接收完数据之后,客户端与服务端通过四次挥手断开连接。
  6. 浏览器渲染:浏览器通过解析HTML构建DOM树——解析CSS资源构建COSSOM树——合并DOM树和CSSOM树生成渲染树——生成渲染树布局——渲染绘制将获取到的页面资源呈现出来。

一、浏览器的基本结构

浏览器大致有以下部分组成:

  1. 用户界面:就是浏览器的外壳,浏览器的界面,包括搜索栏、书签、前进后退按钮、打开历史记录等用户可操作功能;它可以将我们的操作转入浏览器引擎进行处理。
  2. 浏览器引擎:用于接收处理用户界面传递的'操作指令',然后调用相应的渲染引擎;是浏览器中各个部分可以互相通信的核心。
  3. 浏览器渲染引擎:只要负责解析DOM和CSS,然后将内容渲染到页面上;是浏览器最核心的最重要的模块,我们常说的浏览器内核主要就是指渲染引擎。
  4. 网络模块:负责HTTP网络请求或者下载资源的模块。
  5. JS解释器:用于解析和执行JS代码的部分。
  6. 数据持久化存储: 主要把数据存入硬盘,涉及cookie、localstorage等客户端存储技术。
  7. UI后台:绘制基础元件,如选择框、按钮、输入框等。

浏览器的进程和线程

了解了浏览器基本结构之后,我们需要知道浏览器的进程跟线程的概念,通过对浏览器进程和线程的了解,可以帮助我们更加清晰的了解浏览器的运行原理。

什么是进程和线程 :

  1. 进程:CPU资源分配的最小单位(是能拥有资源和独立运行的最小单位,进程之间不会共享资源)
  2. 线程:CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程,多个线程之间共享进程的资源)
  3. 不同进程之间也可以通信,但是代价会比较大

我们通常会用工厂和工人之间的关系来区分进程跟线程:社会将资源和空间分配给不同工厂;每个工厂都有自己不同的资源和空间;每个工厂又会有很多工人,工人没有自己的资源和空间,但是他们使用、共享工厂的资源和空间

浏览器进程的划分:

  1. 浏览器进程(一个): 浏览器的主线程,主要负责浏览器的页面管理、书签、前进后退、资源下载管理以及创建和销毁其他进程等工作,整个浏览器应用程序只有一个,对应上述浏览器组成中的浏览器引擎 。
  2. 渲染进程(多个): 内核进程,负责页面渲染、JS执行,对应的是上述的渲染引擎和JS引擎,一个浏览器可以包含多个渲染进程,每个Tab窗口页对应一个渲染进程。渲染进程下面还划分了多个线程。
  3. GPU进程(一个):负责GPU渲染, 主要用于3D绘制。
  4. 插件进程(多个):浏览器安装的插件(扩展程序),每一个插件都会创建一个进程

渲染进程里的线程划分:

  • GUI渲染线程:负责页面的渲染,解析HTML、CSS,创建DOM树、CSSDOM树、渲染树、绘制、重绘重排等操作;在JS运行期间该线程一直处于挂起状态,等到JS线程空闲时再被执行
  • JS引擎线程:一个页面只会有一个JS引擎(单线程); 负责解析和执行JS代码,处理用户交互,操作DOM树和CSSDOM树等。
  • 事件线程:控制事件循环的线程;当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,排队等待JS引擎处理。事件循环机制
  • 定时器线程:浏览器通过定时器线程来创建一个定时器,完成计时操作(如果放到JS引擎线程,可能会由于阻塞导致计时不准确),计时完毕后,会将任务添加到事件队列,等JS引擎空闲后执行,setInterval与setTimeout所在的线程。
  • HTTP线程:如果请求有回调函数,则把回调函数添加到事件队列,排队等待JS引擎处理 。

注意:GUI渲染进程和JS引擎是互相排斥的,当JS引擎执行时GUI会被挂起,等到JS引擎空闲的时候GUI才会继续被执行,所以当JS线程执行时间过长的话就会造成页面渲染不连贯,产生卡顿现象。

二、HTTP解析过程

在了解了浏览器的基本结构之后,我们就从在浏览器地址栏输入访问地址开始,看一下浏览器都经历了那些事情。大概就是1、DNS解析(找地址);2、与服务端建立连接;3、进行通信;4、断开连接

1.DNS解析

  1. 当我们在地址栏输入一个www.baidu.com网址的时候,浏览器会先在自己的缓存里查看是否有当前地址记录,如果有就直接浏览,没有的话执行step2
  2. 查看当前电脑主机host文件是否存在输入的网址,如果存在就通过主机直接访问,如果没有执行step3
  3. 查找本地DNS缓存,如果存在搜索地址,就返回给主机,再通过主机进行访问,不存在就执行step4
  4. 访问DNS根服务器,根服务器里并没有完成的网址,会将.com这个一级域 返回给本地DNS
  5. 本地DNS接收到.com一级域之后,就去该域服务器进行查找,查找到具体网址baidu.com然后返回
  6. 本地DNS接收到baidu.com去寻找baidu.com解析服务器,然后将www.baidu.com这个完整的IP地址返回给本地DNS缓存服务器
  7. 本地DNS接收到IP地址之后将他存入缓存,以便以后访问
  8. 主机连接接收到的IP地址

其实DNS解析过程就是查找当前网址对应的IP地址的过程;我们常输的网址就是方便记忆的IP地址的映射

2.连接远程服务器(三次握手建立TPC链接)

什么是TCP:TCP说白了就是不同终端实现通信的网络传输控制协议;现有的网络通信方式一般是TCP(传输控制协议),UDP(用户数据报协议);我们大部分应用使用的都是就是TCP传输层协议,相比UDP它更加的安全可靠(UDP只负责发送数据包,不管有没有输送到,虽然效率高但是不可靠)。

TCP连接分为三个步骤:客户端发送 到服务端,服务端收到信息之后发送 信息给客户端,客户端收到信息后发送 信息给服务端。

  1. 客户端与服务端连接时,向服务端发送SYN message(同步信息)。Message 包含sequence number(32位的随机数),ACK=0,window size,最大 segment大小。例如:window size 是 2000 bits,最大segment是200 bits,则最大可传输segments是10 data。
  2. 服务端收到客户端同步请求之后,回复客户端SYN和ACK。ACK 数值是收到客户端的SYN+1。例如:客户端发送的SYN是1000,则服务端回复的 ACK 是 1001。如果服务端也想建立连接,回复中还会包括一个SYN,这个SYN是服务器产生的一个与客户端不同的SYN随机数。这一阶段完成时,客户端与服务端的连接已经建立。
  3. 客户端收到服务端的SYN后,回复服务端ACK,ACK值是服务端传递过来的SYN+1。这一过程完成后,服务端与客户端的连接也建立了起来。
三次握手拟人化描述

如果我们建立的是HTTPS安全连接,则还需要TLS(Transport Layer Security)传输层安全性协议。

对称加密:使用同一个密钥进行加密解密的会话工作,如果被第三方知道了密钥规则就很容易遭到攻击。
非对称加密:非对称加密有不同的密钥,分别是公钥私钥,公钥用来加密,私钥用来解密。服务端把公钥告诉客户端,客户端想要发送数据给服务端的时候需要使用公钥加密数据发送,服务端收到数据再用私钥解密。这样更加的安全

  1. 客户端发起请求:客户端发送 Client hello 给服务端。客户端在Handshake Protocol(会话协议)中会告诉服务端支持的TLS的版本(Version)和支持的加密套件(Cipher Suites,有16个加密套件,可以理解为不同的加密算法组合),并生成第1个随机数A发给服务端。
  2. 服务端响应请求
    1. 服务端接收到客户端数据,保存随机数A,在响应报文里确认TLS版本( Version )、选择使用的加密套件(Cipher Suites)、并生成第2个随机数B发送给客户端。
    2. 接下来服务端会继续发送一个证书(Certificate)给客户端。这样以便客户端判断服务端证书是否存在在自己的信任列表里。
    3. 发完证书之后,再发送一个公钥(Pubkey)给客户端。
    4. 最后告诉客户端发送完毕(Sever hello Done)。
  3. 客户端处理请求响应:拿到服务端证书后,验证证书是否可信;保存从服务端发来的第2个随机数B,生成第3个随机数C,并对使用公钥进行加密 (预主密钥),并且把这个随机数发送给服务器 ;然后告诉服务器就用这套算法进行数据传递;这个时候客户端已经准备好了。
  4. 服务端确认:服务端接收到客户端发送过来的预主密钥,然后用自己的私钥解密,然后获得随机数C,同时告诉客户端表示服务器这边也没问题了,可以进行数据加密交换了

经过以上的非对称加密,最终客户端和服务端都有相同的随机数A,随机数B,随机数C,然后用相同的加密规则对其加密生成会话密钥;最后客户端和服务端就可以放心的根据生成的会话密钥进行对称加密会话了

TLS图解

3.HTTP超文本链接

在客户端和服务端建立了连接之后就可以进行数据传输了。客服端向服务端发起页面请求,服务端返回页面所需资源。新的TCP连接无法立即使用客户端和服务器之间的全部有效带宽。TCP首次往返中,服务器最多只能发送14KB数据,然后必须等待客户端确认已收到这些数据,才能增大拥塞窗口并继续发送更多数据 。这里有一个很重要的点14k,就是我们页面优化时常说的首页14k法则,那么接下来我们就详细了解一下这个14k具体是怎么来的:

TCP作为一个安全可靠的网络传输协议有自己很多的方法和特性来保证网络传输的安全稳定:确认应答、超时重传、连接管理(三次握手四次挥手)、滑动窗口 、 流量控制 、拥塞控制、延迟应答、捎带应答等。如果想了解全部特性可参考TCP协议十大特性。 这里我们着重聊一下拥塞控制

TCP的拥塞控制有四大过程:慢启动拥塞避免快速重传快速恢复。他的主要思想就是:先发送少量数据,传输过程中没有出现网络拥塞,拥塞窗口的值就可以再增大一些,以便把更多的数据包发送出去,出现网络拥塞,拥塞窗口的值就减小一些,以减少数据包的注入。

拥塞控制
•慢启动

由于我们并不能完全预测网路的负荷情况,如果一股脑把大量数据字节注入到网络,那么就有可能引发网络发生拥塞。所以最好的方法是先探测一下,即由小到大逐渐增大发送窗口,等发生网络拥塞时,我们就知道当前网络能达到的最大输出量了。

具体实现就是:当建立一个新的TCP连接时,CWND(拥塞窗口)初始化为1个最大报文段(MSS)大小(这就是首页14k的原因)。发送方开始按照拥塞窗口大小发送数据,每当有一个报文被确认,CWND就增加1个MSS大小。这样随着发送方和接收方这一来一回,CWND的值就翻倍增长(1,2,4,8,...,2^n)。这样CWND由1指数增长的方式就是TCP的慢启动

那么在TCP发送第一条数据的时候(CWND初始化为1),传输的数据为什么最好控制在14k内呢:我们在首次发送TCP的时候最多能发10个包(目前业界能做到的最大值),而一个TCP数据包能传递的最大长度为1500字节,其中标头占了40个字节,剩下1460个字节是一个TCP能传递信息的长度。10个TCP包就是:10 * 1460 = 14600字节,约14kb。

• 拥塞避免

如果一直按照慢启动的规则来增加CWND数量的话,毫无疑问一定会发生网络拥塞。这个时候就需要一定的限制了,TCP使用了一个慢启动门限(ssthresh),当CWND超过该值后,慢启动结束,进入拥塞避免阶段。对于大多数的TCP来说,ssthresh的值是65535(以16bit来计算)。拥塞避免的思想就是将指数增加变为加法线性增加。这样就可以避免增长过快导致网络拥塞,慢慢地增加调整到网络的最佳值。

• 快速重传

这里的快速重传和TCP特性的超时重传是不一样的。 有时候某个报文段会在网络中丢失,但实际上网络并未发生拥塞 ,如果也判定为网络拥塞超时重传,就会大大降低传输速率。快速重传可以让发送方尽早知道个别报文丢失,它要求接收双方不要等自己发送数据的时候稍待确认,而是立即发送确认,即使收到了失序(发送方的seq是连续的)报文也要继续发出确认报文。快速重传算法规定,发送方只要一连收到三个重复确认,就应当立即重传接收方尚未收到的报文。

如下图所示:接收方收到了M1和M2后都分别及时发出了确认。现假定接收方没有收到M3,却收到了M4、M5、M6。按照快重传算法,接收方必须立即发送确认报文,但是接收方现在并没有接收到M3,是不会发送M4确认报文的,这个时候接收方缓存M4,然后重新发送M2确认报文,M5、M6亦是如此,这样发送方一连三次接收到发回的M2重复确认,发送方就知道丢包了,然后立即重传M3

快速重传示例
• 快速恢复

快速恢复并不是单独存在的,它是快速重传的后续处理。发送方接收到3个重复的ACK后,就会开始快速重传,如果还有更多的重复ACK,这个时候就需要快速恢复了:当发送方连续收到三个重复确认时,就执行乘法减小算法,把ssthresh门限减半(即cwnd=ssthresh*/2)。这个时候并不执行慢启动算法,而是执行拥塞避免算法,使拥塞窗口缓慢增大。

快速重传、快速恢复图解

4.断开连接(四次挥手断开TPC链接)

  1. 客户端与服务段断开连接时,会发送信息:FIN=1(终止),seq=a(随机序列号);然后停止发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认,此时客户端处于半关闭状态。
  2. 服务端收到客户端信息后,发出确认信息:ACK=1, seq=b,ack=a+1(确认号);服务端进入CLOSE-WAIT(关闭等待)状态,此时服务端处于半关闭状态,TCP处于半关闭状态。客户端收到这个信息后会FIN-WAIT-2(终止等待2)状态。
  3. 服务端向客户端发起断开确认信息后不会立马进行TCP断连,服务端确保送到客户端得信息全部传输完毕后才发起断开连接信息:FIN=1,ACK=1,seq=c,ack=a+1( 确认号 ) ;此时服务端进入最后确认状态LAST-ACK
  4. 客户端收到服务端信息后,向服务端发送确认信息:ACK=1,seq=a+1,ack=c+1,客户端进入时间等待状态(TIME-WAIT),此时的TCP并未释放掉,需要等2MSL后才关闭客户端连接,这个时候TCP才最终断开
四次挥手拟人化描述
•TCP连接为什么是三次握手?两次、四次不可以么?

TCP三次握手主要是考虑以下几个方面

  1. 避免历史连接:想象一种情况,客户端发起了一个TCP连接请求SYN(seq=a),突然发生一种情况客户端挂掉了,SYN报文还被网络给阻塞了,服务端没有收到信息,这个时候用户又重启客户端重新发送了一个TCP去请求SYN(seq=b),网络畅通之后,服务端首先接收到了请求报文SYN(seq=a)的请求,然后回答客户端报文是ack=a+1,然而这个时候客户端的期望回答是b+1,不符合客户端预期,客户端就会回 RST 报文 ,服务端收到RST报文就可以关闭当前连接;服务端收到seq=b的连接请求后,返回确认报文ack=b+1,符合客户端预期,就可以正常建立连接,如此就很好的规避了一些重复历史连接的问题
  2. 同步双方序列:是保证TCP连接的可靠性的核心。客户端发送一个随机数seq=a给服务端,服务端将他保存起来,服务端再发一个随机数seq=b,客户端收到,客户端将它保存起来,这样客户端和服务端都存了对方和自己的一组随机数,在往后数据传输的时候就可以做到安全可靠避免重复。
  3. 避免浪费资源:在同步了双方序列号之后就已经建立了安全可靠的连接了,再多几次握手也只会是浪费资源,并没有什么太大用处。
•TCP连接的时候是3次,但是为什么关闭的时候却是4次

因为断开连接需要确保客户端和服务端都没有数据发送了。相比TCP连接的三次握手,关闭的时候多了一个服务端确保所有信息传送完毕的动作(第三次挥手)

•客户端发出第四次挥手的确认报文后,为什么要等2MSL的时间才能释放TCP连接

MSL指的是:TCP报文在传输过程中的最大生命周期(Maximum Segment Lifetime)。2MSL就是服务端发出端来连接和客户端发出确认报文,所能保持的最大有效时长。等待2MSL主要是为了丢包的问题:如果第四次挥手报文丢失(服务端没有收到客户端的确认报文),服务端会触发超时重传,重新向客户端发送第三次挥手,客户端再次向服务端发起第四次挥手,这样一来一回就是2MSL,所以客户端需要等这么长时间来确认服务端确实已经收到了。

•如果已经建立了连接,但是客户端突然出现故障了怎么办

TCP设有一个保活计时器,服务端每次收到客户端请求后都会复位这个计时器,若2小时都没接收到客户端的任何请求,服务端就会每隔75秒发送一个探测报文,若一连发送10个探测报文仍然没反应,服务端就认为客户端出了故障,主动断开连接。

关于三次握手和四次挥手的更深入详细介绍可以参考三次握手和四次挥手

三、浏览器渲染过程

1、构建DOM树

当浏览器接收到服务器响应来的HTML文档后,HTML解释器会将收到的字节流转换生成DOM树(DOM Tree)。生成过程:HTML解释器首先将收到的字节流(Bytes)解码成字符流(Characters),然后再通过词法分析解析成词语(Tokens),之后再经过语法分析构建成节点,最后将节点组建成一颗DOM树。

DOM树构建过程

2、构建CSSOM规则树

CSSOM的构建与DOM非常相似,都是解析字节数据最后生成CSSOM。由于元素的样式会受到很多因素的影响(我们设定的样式,通过继承获得样式等),在这过程中浏览器需要递归CSSOM树才能确定元素最终的样式,是非常消耗性能的。所以我们的DOM树尽量要小,CSS尽量用id和class,尽量少去嵌套,保证DOM和CSS的的层级扁平。

CSSOM树构建过程

•渲染阻塞

对DOM树的阻塞:在渲染过程中,如果遇到了<script>就会立刻停止渲染,然后执行JS代码。这是因为GUI渲染进程和JS引擎是互相排斥的,当JS引擎执行时GUI会被挂起,等到JS引擎空闲的时候GUI才会继续被执行。也就是说执行JS代码时,DOM渲染被挂起,JS执行完毕,DOM才渲染继续执行。

对CSSOM树的阻塞:JS是可以操作DOM的样式的,而不完整的CSSOM是无法被使用的,那么在执行JS代码时就必须拿到完整的CSSOM树,这就导致了一个现象:浏览器如果尚未完成CSSOM的下载和构建,我们却要执行JS代码,这个时候浏览器就会延迟JS代码的执行和DOM树的构建,直至完成对CSSOM树下载和构建。也就是说这种情况下浏览器会先构建CSSOM,然后执行JS,最后渲染DOM。这样就很可能会出现白屏。

基于以上两个原因,我们经常会把CSS样式放在HTML最上面,script标签放最HTMl的最底部,这样可以有效的避免DOM树的渲染不被JS给打断。

3、构建渲染树

通过DOM树和CSSOM树我们便可以构建渲染树。浏览器会先从DOM树的根节点开始遍历每个可见节点。对每个可见节点,找到其适配的CSS样式规则并应用。渲染树构建完成后,每个节点都包含节点内容和对应的CSS样式。渲染树是用于显示的,那些不可见的元素不会出现在渲染树上,如:header元素、display=none的元素等;注意visibility=hidden的元素是会出现在渲染树上。

4、渲染树布局

布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。

5、渲染树绘制

在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。

•重绘和回流(重排)

我们都知道HTML默认是流式布局的,但CSS和JS都有可能会打破这种布局,如改变DOM的外观、大小、位置等样式。这就会造成浏览器的重绘和回流

重绘: 渲染树节点发生改变,但不影响该节点在页面当中的空间位置及大小。如某个div标签节点的背景颜色、字体颜色等等发生改变,但是该div标签节点的宽、高、内外边距并不发生变化,此时会触发浏览器的重绘(repaint) 。

回流:当渲染树节点发生改变,影响了节点的几何属性(如宽、高、内边距、外边距、或是布局属性float、position、display:none等),导致节点位置发生变化,此时触发浏览器的回流也称重排(reflow),需要重新生成渲染树。

理论上每一次修改DOM或者DOM的几何属性,都会引起一次浏览器的重排和重绘,而如果修改的只是DOM的非几何属性,则只会引起重绘过程。所以说重排一定会引起重绘,而重绘不一定会引起重排。浏览器的回流和重绘是很消耗性能的,我们在实际开发中应该减少对DOM的操作进而减少回流和重绘的发生。

现代浏览器对回流和重绘的优化:现代浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次,进而减少浏览器的

减少重排的一些方法:

  • 不要一个个的修改 DOM 的样式,应通过 class 来修改。
  • 使用visibility替代display: none 。因为前者只会引起重绘,后者会引发回流。
  • 使用transform替代top
  • 不要用table布局,table一个很小的改动就有可能引起回流。
  • @keyframes动画速度越快,回流次数越多,可以改用requestAnimationFrame
  • 可以将需要多次需要修改的DOM设置成display:none,等所有操作结束后再设置为block
  • 使用文档片段(document fragment)在当前DOM之外构建一个子树,操作完之后,再把它拷贝回文档
野生小园猿
励志做一只遨游在知识海洋里的小白鲨
查看“野生小园猿”的所有文章 →

相关推荐