浏览器渲染机制
浏览器地址栏输入url后发生了什么?...
网络通信
URL解析
浏览器先会判断输入的地址是不是一个合法的URL结构,如果不是,浏览器会使用搜索引擎对这个字符串进行搜索,是的话则进行DNS解析。
DNS解析
缓存判断
判断是正确的URL格式之后,DNS会在我们的缓存(包括浏览器缓存,操作系统缓存等)中查询是否有当前域名的IP地址。
如果命中缓存则直接会直接从缓存中拿取对应的IP地址,如果是强缓存,则直接返回缓存资源,跳过以下所有步骤。
没有命中则执行IP查询。
IP查询
依靠DNS协议(基于
udp
)通过迭代查询
和递归查询
结合使用的方式将域名解析为IP地址。为什么dns解析是基于udp而非tcp协议?
dns解析过程是一个服务器的查找过程。因为域名分为一级/二级...域名,所以每一级域名都会迭代去查询。如果它采用tcp协议的话,每经过一次域名查询,域名服务器都会经过三次握手,四次挥手。但是udp就不会,他会直接发包然后确认。
相较于udp,tcp是更加安全,可靠的,但是这也造成了它相对于udp消耗更多时间。
网络请求
建立连接
(1)TCP三次握手
其中:
- SYN:表示请求建立连接
- Seq:随机序列号
- ACK:上一次握手的Seq+1
1:由浏览器B发起,告诉服务器S:我要给你发送请求(
seq=x
)2:由服务器S发起,告诉客户端B:收到了B的请求(
seq=y,ack=seq+1
),我已经准备接受了,你可以发送了3:由浏览器B发送,告诉服务器S:收到了S的回复(
seq=z,ack=seq+1
),那我马上发送给你为什么不能是2次握手
假如只有2次握手:
- 客户端发送第一个SYN包时如果发送阻塞,超时后会重新发送SYN包(二者标识一致)。服务端接收了重传的SYN包然后回传了第二次握手并建立了连接。后续如果阻塞的SYN包(此时理应已被视作无效连接)消除阻塞到达了服务端,会让客户端和服务器再次建立连接但却是无意义的。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了第二次握手,但是客户端不会再次发出确认。由于服务器收不到确认,所以没有建立连接。
防止了已失效的连接请求报文段,避免资源浪费
- tcp是全双工通信。第二次握手结束后,浏览器知道服务端收到了消息并回复了自己,但是服务端并不知道自己回复的消息对方是否收到,
无法保证接下来的通信是否可靠
。
为什么不应该是4次握手
第1,2次之后,客户端确保了自己的发送和接收无问题。
第3次之后,服务端确保了自己的发送(第2次握手回复的消息被收到了)和接收无问题。
所以无需第4次。
(2) TLS四次握手(
仅https
)HTTPS 建立连接的过程,先进行 TCP 三次握手,再进行 TLS 四次握手:
1:客户端发出请求,向服务端提供客户端支持的
协议版本
(如TLS 1.0),加密方法
(如RSA公钥加密),随机数
(client random
,用于生成对话密钥)等信息。2:服务器回复请求,向客户端确认协议版本,加密方法。并发送服务器证书、非对称加密的公钥,以及另一个随机数(
server random
)。3:客户端收到服务器证书并验证,有问题会向访问用户警告是否继续通信。如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串新的随机数(
premaster secret
)并通过服务端下发的公钥及加密方法进行加密,然后发送给服务端。并由此前三个随机数通过一定的算法来生成“会话密钥”
(Session Key)。4:服务器收到客户端发送的数据(premaster secret),此时服务端也拿到了三个随机数,使用同样的算法计算出 “会话密钥”(Session Key)。
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用 “会话密钥” 加密内容。
发起请求
建立连接后,开始正式发起HTTP/HTTPS请求,此过程详见:HTTP协议
页面渲染
请求完成后,正常情况浏览器会收到服务端返回的html,css,js以及图片等静态资源。
HTML解析
解析HTML,会构建Dom Tree
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<p>p标签的内容</p>
<div>div标签的内容</div>
</body>
</html>
解析HTML过程中,当遇到script
标签时:
- 默认情况下会阻塞HTML和CSS的解析(因为script可能会操作dom和样式,所以通常
script标签建议放在body元素后面
),并且会等待script脚本下载并执行完毕,然后继续解析HTML。 - 如果script标签含有
defer
或者async
属性,则不会阻塞HTML解析。此时script标签就无需放在body元素后面。
TIP
defer
:会并行下载js脚本,下载过程不阻塞html解析,且等待HTML解析完成,且在DOMContentLoaded
事件之前才执行该脚本。所以可放心访问和操作dom。
async
:会并行下载js脚本,下载过程不阻塞html解析,但下载完成后会立即执行该脚本,故执行顺序不可控。涉及dom操作可能会报错。
CSS解析
解析HTML过程中,当遇到CSS Link标签时,会由浏览器负责下载对应的CSS文件,此时并不会阻塞HTML的解析,下载完成后进行CSS解析,并构建CSSOM Tree
(CSS Object Model,CSS对象模型),也称Style Rules
。
构建Render Tree
有了DOM Tree 和 CSSOM Tree 后,就可以两个结合来构建Render Tree
。
注意
CSS Link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程,即Render Tree构建前,先前的CSS必须解析完成。所以Link标签通常放在Dom(通常为body元素)上方
,否则会造成二次渲染。
布局(layout)
根据Render Tree中的渲染对象的信息,计算出每一个渲染对象的位置和尺寸。
绘制(paint)
将布局阶段计算的每个frame转为屏幕上实际的像素点。
重排 & 重绘
script脚本后续交互对dom和样式的操作会涉及到二次布局与绘制。
(1)重排(回流)
元素的尺寸、位置、布局和页面的结构发生改变时浏览器触发的行为,比如:
- 页面初始渲染(开销最大的一次重排)
- 添加/删除可见的DOM元素;
- 改变元素位置;
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
- 改变元素内容,比如文字数量,图片大小等;
- 改变元素字体大小;
- 改变浏览器窗口尺寸,比如resize事件发生时;
- 激活CSS伪类(例如::hover);
- 设置 style 属性的值
- ...
重排是一种比较昂贵
的操作,因为它会触发浏览器重新计算并重新绘制元素,可能导致性能下降。
如何减少重排
- 使用
class类名切换
来代替style动态修改的操作,从而实现样式集中修改。 - 如果必须使用style动态修改且修改次数过多,则可先将
display:none
,再修改style,再还原display,此举可减少重排次数。 - 元素动画或变形的场景需求,尽量使用
transform
属性,因为它不会影响布局,减少直接改变元素实尺寸,位置的次数。 - 使用
absolute
或fixed
让元素脱离普通文档流,使该元素单独成为渲染树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响。 - 使用
requestAnimationFrame
来执行动画,它可以帮助你在浏览器下一次重新渲染之前执行动画操作。这可以减少回流和重绘的发生,因为浏览器会将多个操作合并到一次渲染中。
(2)重绘
是当元素样式发生改变,但没有影响其布局时,浏览器只需要重新绘制元素的外观的过程,比如以下样式属性发生改变都会引发重绘:
- color
- border-style
- border-radius
- text-decoration
- box-shadow
- outline
- background
- ...
相比于重排,重绘是一种较为轻量级
的操作。
断开连接
在渲染完成后,浏览器可能会继续加载页面中的其他资源,如异步加载的内容或者通过JavaScript生成的动态内容。
而在此过程中,如果没有其他资源需要加载,浏览器将与服务器之间的TCP连接断开,即“四次挥手”:
其中:
- FIN:表示请求断开连接
- Seq:随机序列号
- ACK:上一次挥手的Seq+1
1:由浏览器B发起,告诉服务器S:我的请求数据都发送完了,打算关闭连接了(seq=u
)
2:由服务器S发起,告诉客户端B:我收到了。我这边还有部分数据没接收,等我消息...(seq=v,ack=u+1
)
3:由服务器S发起,告诉客户端B:我已经接收完毕,你再确认下,没问题就可以关闭了(seq=w,ack=v+1
)
4:由浏览器B发起,告诉服务器S:确认完毕!(seq=u+1,ack=w+1
)
随后,服务器正式断开连接,而客户端经过 2MSL
一段时间后,也正式断开连接
为什么不是3挥手或5次挥手?
如果只有3次挥手,可能会出现以下问题:
- 服务器
可能还有未传输完的数据
,但客户端已经关闭了数据传输通道,导致数据丢失。 - 客户端可能未收到服务器的关闭请求,导致连接无法正常关闭。
而5次挥手则是不必要的,因为4次挥手已经足够确保双方正确关闭连接,并且保证数据的可靠传输。
为什么客户端最后需要等待2MSL才能关闭连接?
当客户端发出最后的 ACK 确认报文时,并不能确定服务器端能够收到该段报文
。 所以客户端在发送完 ACK 确认报文之后,会设置一个时长为 2MSL
的计时器。
MSL
指的是 Maximum Segment Lifetime:一段 TCP 报文在传输过程中的最大生命周期。
2MSL
即是服务器端发出为 FIN 报文和客户端发出的 ACK 确认报文所能保持有效的最大时长。
服务器端在 1MSL 内没有收到客户端发出的 ACK 确认报文,就会再次向客户端发出 FIN 报文。 如果客户端在 2MSL 内,再次收到了来自服务器端的 FIN 报文,说明服务器端由于各种原因没有接收到客户端发出的 ACK 确认报文。 则客户端会再次向服务器端发出 ACK 确认报文,计时器重置,重新开始 2MSL 的计时。
如果客户端在 2MSL 内没有再次收到来自服务器端的 FIN 报文,说明服务器端正常接收了 ACK 确认报文,客户端可以正式断开,完成“四次挥手”。
注意
客户端和服务端都可能主动发起断开连接,不一定是客户端