对于前端开发来说,熟悉浏览器的运行原理是必须要掌握的。本文将尝试从浏览器基本结构、HTTP解析流程、浏览器渲染流程、浏览器缓存策略、浏览器存储等几个方面归纳总结浏览器的基本工作原理。在进行正文之前我们可以先了解一下,当我们在浏览器地址栏输入网站地址时,浏览器所经历的大致流程:
了解了浏览器基本结构之后,我们需要知道浏览器的进程跟线程的概念,通过对浏览器进程和线程的了解,可以帮助我们更加清晰的了解浏览器的运行原理。
什么是进程和线程 :
我们通常会用工厂和工人之间的关系来区分进程跟线程:社会将资源和空间分配给不同工厂;每个工厂都有自己不同的资源和空间;每个工厂又会有很多工人,工人没有自己的资源和空间,但是他们使用、共享工厂的资源和空间
浏览器进程的划分:
渲染进程里的线程划分:
注意:GUI渲染进程和JS引擎是互相排斥的,当JS引擎执行时GUI会被挂起,等到JS引擎空闲的时候GUI才会继续被执行,所以当JS线程执行时间过长的话就会造成页面渲染不连贯,产生卡顿现象。
在了解了浏览器的基本结构之后,我们就从在浏览器地址栏输入访问地址开始,看一下浏览器都经历了那些事情。大概就是1、DNS解析(找地址);2、与服务端建立连接;3、进行通信;4、断开连接
其实DNS解析过程就是查找当前网址对应的IP地址的过程;我们常输的网址就是方便记忆的IP地址的映射
什么是TCP:TCP说白了就是不同终端实现通信的网络传输控制协议;现有的网络通信方式一般是TCP(传输控制协议),UDP(用户数据报协议);我们大部分应用使用的都是就是TCP传输层协议,相比UDP它更加的安全可靠(UDP只负责发送数据包,不管有没有输送到,虽然效率高但是不可靠)。
TCP连接分为三个步骤:客户端发送 到服务端,服务端收到信息之后发送 信息给客户端,客户端收到信息后发送 信息给服务端。
如果我们建立的是HTTPS安全连接,则还需要TLS(Transport Layer Security)传输层安全性协议。
对称加密:使用同一个密钥进行加密解密的会话工作,如果被第三方知道了密钥规则就很容易遭到攻击。
非对称加密:非对称加密有不同的密钥,分别是公钥和私钥,公钥用来加密,私钥用来解密。服务端把公钥告诉客户端,客户端想要发送数据给服务端的时候需要使用公钥加密数据发送,服务端收到数据再用私钥解密。这样更加的安全
经过以上的非对称加密,最终客户端和服务端都有相同的随机数A,随机数B,随机数C,然后用相同的加密规则对其加密生成会话密钥;最后客户端和服务端就可以放心的根据生成的会话密钥进行对称加密会话了
在客户端和服务端建立了连接之后就可以进行数据传输了。客服端向服务端发起页面请求,服务端返回页面所需资源。新的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)。这个时候并不执行慢启动算法,而是执行拥塞避免算法,使拥塞窗口缓慢增大。
TCP三次握手主要是考虑以下几个方面
因为断开连接需要确保客户端和服务端都没有数据发送了。相比TCP连接的三次握手,关闭的时候多了一个服务端确保所有信息传送完毕的动作(第三次挥手)
MSL指的是:TCP报文在传输过程中的最大生命周期(Maximum Segment Lifetime)。2MSL就是服务端发出端来连接和客户端发出确认报文,所能保持的最大有效时长。等待2MSL主要是为了丢包的问题:如果第四次挥手报文丢失(服务端没有收到客户端的确认报文),服务端会触发超时重传,重新向客户端发送第三次挥手,客户端再次向服务端发起第四次挥手,这样一来一回就是2MSL,所以客户端需要等这么长时间来确认服务端确实已经收到了。
TCP设有一个保活计时器,服务端每次收到客户端请求后都会复位这个计时器,若2小时都没接收到客户端的任何请求,服务端就会每隔75秒发送一个探测报文,若一连发送10个探测报文仍然没反应,服务端就认为客户端出了故障,主动断开连接。
关于三次握手和四次挥手的更深入详细介绍可以参考三次握手和四次挥手
当浏览器接收到服务器响应来的HTML文档后,HTML解释器会将收到的字节流转换生成DOM树(DOM Tree)。生成过程:HTML解释器首先将收到的字节流(Bytes)解码成字符流(Characters),然后再通过词法分析解析成词语(Tokens),之后再经过语法分析构建成节点,最后将节点组建成一颗DOM树。
CSSOM的构建与DOM非常相似,都是解析字节数据最后生成CSSOM。由于元素的样式会受到很多因素的影响(我们设定的样式,通过继承获得样式等),在这过程中浏览器需要递归CSSOM树才能确定元素最终的样式,是非常消耗性能的。所以我们的DOM树尽量要小,CSS尽量用id和class,尽量少去嵌套,保证DOM和CSS的的层级扁平。
1 2 3 4 5 6 7 8 9 10 |
<div> <a> <span></span> </a> </div> <style> span {//只需要找到全局span标签,并设置样式就行 color: red; } div > a > span {//需要找到全局span,然后找到span标签上的a标签,然后再找到a标签上的div标签,最后再给符合条件的span设置样式,这样就多了几层递浪费了性能 color: red; </style> |
对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给打断。
通过DOM树和CSSOM树我们便可以构建渲染树。浏览器会先从DOM树的根节点开始遍历每个可见节点。对每个可见节点,找到其适配的CSS样式规则并应用。渲染树构建完成后,每个节点都包含节点内容和对应的CSS样式。渲染树是用于显示的,那些不可见的元素不会出现在渲染树上,如:header元素、display=none的元素等;注意visibility=hidden的元素是会出现在渲染树上。
布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小与位置,布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。
在绘制阶段,遍历渲染树,调用渲染器的paint()方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
我们都知道HTML默认是流式布局的,但CSS和JS都有可能会打破这种布局,如改变DOM的外观、大小、位置等样式。这就会造成浏览器的重绘和回流
重绘: 渲染树节点发生改变,但不影响该节点在页面当中的空间位置及大小。如某个div标签节点的背景颜色、字体颜色等等发生改变,但是该div标签节点的宽、高、内外边距并不发生变化,此时会触发浏览器的重绘(repaint) 。
回流:当渲染树节点发生改变,影响了节点的几何属性(如宽、高、内边距、外边距、或是布局属性float、position、display:none等),导致节点位置发生变化,此时触发浏览器的回流也称重排(reflow),需要重新生成渲染树。
理论上每一次修改DOM或者DOM的几何属性,都会引起一次浏览器的重排和重绘,而如果修改的只是DOM的非几何属性,则只会引起重绘过程。所以说重排一定会引起重绘,而重绘不一定会引起重排。浏览器的回流和重绘是很消耗性能的,我们在实际开发中应该减少对DOM的操作进而减少回流和重绘的发生。
现代浏览器对回流和重绘的优化:现代浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次,进而减少浏览器的
减少重排的一些方法:
浏览器缓存是指:浏览器在访问网站的时候,将一部分数据存储在浏览器本地空间的行为 ,如html,css,js,img等 。浏览器缓存更多的是浏览器和后端之间的交互策略,前端操作空间不大,但是理解浏览器的缓存策略对于前端来说至关重要。
缓存顺序: Service Worker > Memory Cache > Disk Cache > Push Cache ;网络请求根据以上顺序从缓存里查找有没有网络缓存,如果有就直接取用如果没有就请求网络
强制缓存
强制缓存是指:浏览器在进行资源请求的时候,先检查缓存里面有没有该资源的缓存,如果该资源缓存存在并没有过期,就直接使用该资源缓存起来的数据,否则就重新请求资源拿到数据进行缓存。
实现强制缓存的方式: 强缓存的实现依赖于HTTP响应头中的两个字段:Expires和Cache-Control。Expires(HTTP1.0)是一个具体的过期时间,浏览器会根据该时间判断资源是否过期,例如: Expires : 1724509582表示:该缓存在2024/8/24前有效,浏览器通过比较当前时间和Expires失效时间,来确定缓存是否有效;Cache-Control(HTTP1.1)是一个相对时间,可以指定资源的有效时间,例如:Cache-Control:max-age=2592000表示:该缓存在2592000秒后过期,浏览器会记录初始请求资源的时间戳,当前请求资源的时间戳 > 初始请求时间戳+ 2592000,则强制缓存过期,重新请求资源更新缓存数据。 在同时设置有 Cache-Control和Expires的请求中:Cache-Control > Expires。
Cache-Control主要用于控制页面缓存,主要有以下属性
协商缓存
协商缓存是指 :当浏览器请求资源的时候,发现有缓存数据,且缓存结果失效,此时浏览器会携带对应的缓存数据发送 HTTP 请求, 通过对比缓存数据的Last-Modified(后台数据最后一次改变的时间)或者Etag(后台数据最后一次改变的hash值)与服务器上的数据是否一致来确定是否使用缓存。如果数据一致,服务器返回304,浏览器继续使用缓存中的数据作为请求数据;如果数据不一致,服务器返回200和最新的数据。