从URL输入到页面展现到底发生什么?
从URL输入到页面展现到底发生什么?
总体来说分为以下几个过程:
- 解析URL并生成请求报文
- DNS 解析:将域名解析成IP地址
- TCP 三次握手、TLS三次握手 =》在HTTPS上建立安全连接
- 发送HTTP请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP四次挥手
URL解析并生成HTTP请求消息
首先浏览器做的第一步工作就是要对URL进行解析,从而生成发送给Web服务器的请求信息。
URL是什么?
URI 和 URL
- URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
- URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。
URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。
URL遵守以下的语法规则:scheme://host.domain:port/path/filename
- scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。
- host - 定义域主机(http 的默认主机是 www)
- domain - 定义因特网域名,比如
w3school.com.cn
- port - 定义主机上的端口号(http 的默认端口号是 80)
- path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
- filename - 定义文档/资源的名称
当没有路径名时,就代表访问根目录下事先设置的默认文件,也就是/index.html
或者/default.html
这些文件,这样就不会发生混乱了。
生成HTTP请求消息
对URL进行解析之后,浏览器确定了Web服务器和文件名,接下来就是根据这些信息来生成HTTP请求消息了。
域名解析(DNS)
通过浏览器解析URL并生成HTTP消息后,需要委托操作系统将消息发送给Web服务器。
但在发送之前,还有一项工作需要完成,那就是查询服务器域名对应的IP地址,因为委托操作系统发送消息时,必须提供通信对象的IP地址。
比如我们打电话的时候,必须要知道对方的电话号码,但由于电话号码难以记忆,所以通常我们会将对方电话号+姓名保存在通讯录里。
所以,有一种服务器就专门保存了Web服务器域名与IP的对应关系,它就是DNS服务器。
域名的层级关系
DNS 中的域名都是用句点来分隔的,比 www.Serve.com,在域名中,**越靠右的位置表示其层级越高。**
根域的DNS 服务器信息保存在互联网中所有的 DNS服务器中。
这样一来,任何DNS服务器就都可以找到并访问根域 DNS服务器了。
因此,客户端只要能够找到任意一台DNS服务器,就可以通过它找到根域DNS服务器,然后再一路顺藤摸瓜找到位于下层的某台目标 DNS服务器。
IP 地址
IP 地址是指互联网协议地址,是 IP Address 的缩写。
IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。IP 地址是一个 32 (8*4)位的二进制数,比如 127.0.0.1 为本机 IP。
域名就相当于 IP 地址乔装打扮的伪装者,带着一副面具。它的作用就是便于记忆和沟通的一组服务器的地址。
用户通常使用主机名或域名来访问对方的计算机,而不是直接通过 IP 地址访问。
因为与 IP 地址的一组纯数字相比,用字母配合数字的表示形式来指定计算机名更符合人类的记忆习惯。
但要让计算机去理解名称,相对而言就变得困难了。
因为计算机更擅长处理一长串数字。为了解决上述的问题,DNS 服务应运而生。
什么是域名解析
DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。
DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。
1 | 例如 baidu.com 220.114.23.56(服务器外网IP地址)80(服务器端口号) |
浏览器如何通过域名去查询 URL 对应的 IP 呢
- 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。
- 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。
- 路由缓存:路由器也有 DNS 缓存。
- ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求。
- 根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询。
DNS域名解析的过程蛮有意思的,整个过程就和我们日常生活中找人问路的过程类似,只指路不带路。
DNS负载均衡
比如访问baidu.com的时候,每次响应的并非是同一个服务器(IP地址不同),一般大公司都有成百上千台服务器来支撑访问。DNS可以返回一个合适的机器的IP给用户,例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等,这种过程就是DNS负载均衡。
协议栈
通过DNS获取到IP后,就可以把 HTTP的传输工作交给操作系统中的协议栈。
应用程序(浏览器)通过调用Socket库,来委托协议栈工作。
协议栈的上半部分有两块,分别是负责收发数据的TCP和UDP协议,它们会接受应用层的委托执行收发数据的操作。
协议栈的下面一半是用IP协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由IP负责的。
此外IP中还包括ICMP协议和ARP协议。
- ICMP用于告知网络包传送过程中产生的错误以及各种控制信息。
- ARP用于根据IP地址查询相应的以太网MAC地址。
IP下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。
TCP三次握手
在HTTP传输数据之前,首先需要TCP建立连接,TCP连接的建立,通常称为三次握手。
这个所谓的「连接」,只是双方计算机里维护一个状态机。
我们来看看RFC 793是如何定义「连接」的:
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个TCP连接是需要客户端与服务器端达成上述三个信息的共识。
- Socket:由IP地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
TCP
首先,源端口号和目标端口号是不可少的,如果没有这两个端口号,数据就不知道应该发给哪个应用。
接下来有包的序号,这个是为了解决包乱序的问题。
还有就是确认号,目的是确认发出去对方是否有收到。如果没有收到就应该重新发送,直到送达,这个是为了解决不丢包的问题。
接下来还有一些状态位。例如SYN
是发起一个连接,ACK
是回复,RST
是重新连接,FIN
是结束连接等。
TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
还有一个重要的就是窗口大小。TCP要做流量控制,通信双方各声明一个窗口(缓存大小) ,标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。
除了做流量控制以外,TCP还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。
三次握手
- 一开始,客户端和服务端都处于
CLOSED
状态。先是服务端主动监听某个端口,处于LISTEN
状态。 - 客户端会随机初始化序号(
client_isn
) ,将此序号置于TCP首部的序号字段中,同时把SYN
标志位置为1
,表示 SYN 报文。接着把第一个SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。 - 服务端收到客户端的
SYN
报文后,也随机初始化自己的序号(server_isn
),将此序号填入TCP首部的序号字段中,其次把TCP首部的确认应答号字段填入client_isn +1
,接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。 - 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文TCP首部
ACK
标志位置为1
,其次确认应答号字段填入server_isn +1
,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于ESTABLISHED
状态。 - 服务器收到客户端的应答报文后,也进入
ESTABLISHED
状态。
为什么是三次握手?不是两次、四次?
相信大家比较常回答的是︰“因为三次握手才能保证双方具有接收和发送的能力。”
这回答是没问题,但这回答是片面的,并没有说出主要的原因。
在前面我们知道了什么是TCP连接:
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立TCP连接。
接下来以三个方面分析三次握手的原因:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
避免历史连接
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
客户端连续发送多次SYN建立连接的报文,在网络拥堵情况下:
- 一个旧SYN报文比最新的SYN报文早到达了服务端;
- 那么此时服务端就会回一个SYN +ACK报文给客户端;
- 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送RST报文给服务端,表示中止这一次连接。
如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接︰
- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是RST报文,以此中止历史连接;
- 如果不是历史连接,则第三次发送的报文是ACK报文,通信双方就会成功建立连接
同步双方初始序列号
TCP协议的通信双方,都必须维护一个序列号,序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中,哪些是已经被对方收到的;
可见,序列号在TCP 连接中占据着非常重要的作用,所以当客户端发送携带初始序列号的 SYN 报文的时候,需要服务端回一个ACK应答报文,表示客户端的SYN报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
避免资源浪费
如果只有两次握手,当客户端的SYN请求连接在网络中阻塞,客户端没有接收到ACK报文,就会重新发送SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK确认信号,所以每收到一个SYN就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的SYN 阻塞了,重复发送多次SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
小结
TCP建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用两次握手和四次握手的原因:
- 两次握手︰无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 四次握手︰三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
为什么客户端和服务端的初始序列号ISN是不相同的?
如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。
所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。
另一方面是为了安全性,防止黑客伪造的相同序列号的TCP报文被对方接收。
TCP分割数据
如果HTTP请求消息比较长,超过了MSS的长度,这时TCP就需要把HTTP的数据拆解成一块块的数据发送,而不是一次性发送所有数据。
- MTU :一个网络包的最大长度,以太网中一般为1500字节。
- MSS:除去IP和TCP头部之后,一个网络包所能容纳的TCP数据的最大长度。
数据会被以MSS的长度为单位进行拆分,拆分出来的每一块数据都会被放进单独的网络包中。也就是在每个被拆分的数据加上TCP头信息,然后交给IP模块来发送数据。
TCP报文生成
TCP协议里面会有两个端口,一个是浏览器监听的端口((通常是随机生成的),一个是Web服务器监听的端口(HTTP默认端口号是80 ,HTTPS默认端口号是443 ) 。
在双方建立了连接后,TCP报文中的数据部分就是存放HTTP头部+数据,组装好TCP报文之后,就需交给下面的网络层处理。
TLS 协商
为了在HTTPS上建立安全连接,另一种握手是必须的。更确切的说是TLS协商 ,它决定了什么密码将会被用来加密通信,验证服务器,在进行真实的数据传输之前建立安全连接。在发送真正的请求内容之前还需要三次往返服务器。
虽然建立安全连接对增加了加载页面的等待时间,对于建立一个安全的连接来说,以增加等待时间为代价是值得的,因为在浏览器和web服务器之间传输的数据不可以被第三方解密。
经过8次往返,浏览器终于可以发出请求。
TCP慢开始和拥塞控制
一旦我们建立了到web服务器的连接,浏览器就代表用户发送一个初始的HTTP GET请求,对于网站来说,这个请求通常是一个HTML文件。 一旦服务器收到请求,它将使用相关的响应头和HTML的内容进行回复。
初始请求的响应包含所接收数据的第一个字节。
Time to First Byte(TTFB)是用户通过点击链接进行请求与收到第一个HTML包之间的时间。
第一个响应包是14kb大小。这是慢开始的一部分,慢开始是一种均衡网络连接速度的算法。慢开始逐渐增加发送数据的数量直到达到网络的最大带宽。
在”TCP slow start”中,在收到初始包之后, 服务器会将下一个包的大小加倍到大约28kb。 后续的包依次是前一个包大小的二倍直到达到预定的阈值,或者遇到拥塞。
如果您听说过初始页面加载的14Kb规则,TCP慢开始就是初始响应为14Kb的原因,也是为什么web性能优化需要将此初始14Kb响应作为优化重点的原因。TCP慢开始逐渐建立适合网络能力的传输速度,以避免拥塞。
当服务器用TCP包来发送数据时,客户端通过返回确认帧来确认传输。由于硬件和网络条件,连接的容量是有限的。 如果服务器太快地发送太多的包,它们可能会被丢弃。意味着,将不会有确认帧的返回。服务器把它们当做确认帧丢失。拥塞控制算法使用这个发送包和确认帧流来确定发送速率。
IP
TCP模块在执行连接、收发、断开等各阶段操作时,都需要委托IP模块将数据封装成网络包发送给通信对象。
在IP协议里面需要有源地址IP和目标地址IP:
- 源地址IP,即是客户端输出的IP地址;
- 目标地址,即通过DNS域名解析得到的Web服务器IP。
因为HTTP是经过TCP传输的,所以在IP包头的协议号,要填写为06(十六进制),表示协议为TCP。
MAC
生成了IP头部之后,接下来网络包还需要在IP头部的前面加上MAC头部。
MAC头部是以太网使用的头部,它包含了接收方和发送方的MAC地址等信息。
在MAC包头里需要发送方MAC地址和接收方目标MAC地址,用于两点之间的传输。
网卡
网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。因此,我们需要将数字信息转换为电信号,才能在网线上传输,也就是说,这才是真正的数据发送过程。
负责执行这一操作的是网卡,要控制网卡还需要靠网卡驱动程序。
网卡驱动从IP模块获取到包之后,会将其复制到网卡内的缓存区中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。
- 起始帧分界符是一个用来表示包起始位置的标记
- 末尾的FCS(帧校验序列)用来检查包传输过程是否有损坏
最后网卡会将包转为电信号,通过网线发送出去。
交换机
交换机的设计是将网络包原样转发到目的地。交换机工作在MAC层,也称为二层网络设备。
交换机根据MAC地址表查找 MAC地址,然后将信号发送到相应的端口。
路由器
网络包经过交换机之后,现在到达了路由器,并在此被转发到下一个路由器或目标设备。
这一步转发的工作原理和交换机类似,也是通过查表判断包转发的目标。
不过在具体的操作过程上,路由器和交换机是有区别的。
- 因为路由器是基于IP设计的,俗称三层网络设备,路由器的各个端口都具有MAC地址和IP地址;
- 而交换机是基于以太网设计的,俗称二层网络设备,交换机的端口不具有MAC地址。
路由器的端口具有MAC地址,因此它就能够成为以太网的发送方和接收方;
同时还具有IP地址,从这个意义上来说,它和计算机的网卡是一样的。
当转发包时,首先路由器端口会接收发给自己的以太网包,然后路由表查询转发目标,再由相应的端口作为发送方将以太网包发送出去。
服务器处理请求并返回 HTTP 报文
数据包抵达服务器后,服务器会先扒开数据包的MAC头部,查看是否和服务器自己的MAC地址符合,符合就将包收起来。
接着继续扒开数据包的IP头,发现IP地址符合,根据IP头中协议项,知道自己上层是TCP协议。
于是,扒开TCP的头,里面有序列号,需要看一看这个序列包是不是我想要的,如果是就放入缓存中然后返回一个ACK,如果不是就丢弃。TCP头部里面还有端口号,HTTP的服务器正在监听这个端口号。
于是,服务器自然就知道是HTTP进程想要这个包,于是就将包发给HTTP进程。
服务器的HTTP进程看到,原来这个请求是要访问一个页面,于是就把这个网页封装在HTTP响应报文里。
HTTP响应报文也需要穿上TCP、IP、MAC头部,不过这次是源地址是服务器IP地址,目的地址是客户端P地址。
浏览器解析渲染页面
浏览器拿到HTTP响应报文后,接下来介绍下浏览器渲染机制
浏览器解析渲染页面分为一下五个步骤:
- 根据 HTML 解析出 DOM 树
- 根据 CSS 解析生成 CSS 规则树(CSSOM)
- 结合 DOM 树和 CSS 规则树,生成渲染树
- 根据渲染树计算每一个节点的信息
- 根据计算好的信息绘制页面
根据 HTML 解析出 DOM 树
第一步是处理HTML标记并构造DOM树。HTML解析涉及到标记化和树的构造。
HTML标记包括开始和结束标记,以及属性名和值。
如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建DOM树。
DOM树描述了文档的内容。<html>
元素是第一个标签也是文档树的根节点。
树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点。
DOM节点的数量越多,构建DOM树所需的时间就越长。
当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。
当遇到一个CSS文件时,解析也可以继续进行。
但是对于<script>
标签(特别是没有 async
或者 defer
属性)会阻塞渲染并停止HTML的解析。
尽管浏览器的预加载扫描器加速了这个过程,但过多的脚本仍然是一个重要的瓶颈。
浏览器构建DOM树时,这个过程占用了主线程。
当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如CSS、JavaScript和web字体。
多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用来请求它。
它将在后台检索资源,以便在主HTML解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。
预加载扫描仪提供的优化减少了阻塞。
根据 CSS 解析生成 CSS 规则树(CSSOM)
第二步是处理CSS并构建CSSOM树。CSS对象模型和DOM是相似的。
DOM和CSSOM是两棵树. 它们是独立的数据结构。
浏览器将CSS规则转换为可以理解和使用的样式映射。
浏览器遍历CSS中的每个规则集,根据CSS选择器创建具有父、子和兄弟关系的节点树。
与HTML一样,浏览器需要将接收到的CSS规则转换为可以使用的内容。因此,它重复了HTML到对象的过程,但这是对于CSS的。
CSSOM树包括来自用户代理样式表的样式。
浏览器从适用于节点的最通用规则开始,并通过应用更具体的规则递归地优化计算的样式。换句话说,它级联属性值。
构建CSSOM非常非常快,并且在当前的开发工具中没有以独特的颜色显示。
相反,开发人员工具中的“重新计算样式”显示解析CSS、构造CSSOM树和递归计算计算样式所需的总时间。
在web性能优化方面,它是可轻易实现的,因为创建CSSOM的总时间通常小于一次DNS查找所需的时间。
JavaScript 编译
当CSS被解析并创建CSSOM时,其他资源,包括JavaScript文件正在下载(多亏了preload scanner)。JavaScript被解释、编译、解析和执行。脚本被解析为抽象语法树。
渲染
渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。
在解析步骤中创建的CSSOM树和DOM树组合成一个Render树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。
在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在GPU而不是CPU上绘制屏幕的一部分来提高性能,从而释放主线程。
Style
第三步是将DOM和CSSOM组合成一个Render树,计算样式树或渲染树从DOM树的根开始构建,遍历每个可见节点。
像<head>
和它的子节点以及任何具有display: none
样式的结点,例如script { display: none; }
这些标签将不会显示,也就是它们不会出现在Render树上。
具有visibility: hidden
的节点会出现在Render树上,因为它们会占用空间。
每个可见节点都应用了其CSSOM规则。
Render树保存所有具有内容和计算样式的可见节点——将所有相关样式匹配到DOM树中的每个可见节点,并根据CSS级联确定每个节点的计算样式。
Layout
第四步是在渲染树上运行布局以计算每个节点的几何体。
布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。
回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
构建渲染树后,开始布局。渲染树标识显示哪些节点(即使不可见)及其计算样式,但不标识每个节点的尺寸或位置。
为了确定每个对象的确切大小和位置,浏览器从渲染树的根开始遍历它。
在网页上,大多数东西都是一个盒子。不同的设备和不同的桌面意味着无限数量的不同的视区大小。
在此阶段,考虑到视区大小,浏览器将确定屏幕上所有不同框的尺寸。
以视区的大小为基础,布局通常从body开始,用每个元素的框模型属性排列所有body的子孙元素的尺寸,为不知道其尺寸的替换元素(例如图像)提供占位符空间。
第一次确定节点的大小和位置称为布局。
随后对节点大小和位置的重新计算称为回流。
在我们的示例中,假设初始布局发生在返回图像之前。由于我们没有声明图像的大小,因此一旦知道图像大小,就会有回流。
Paint
最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为首次有意义的绘制(FMP) 。
在绘制或光栅化阶段,浏览器将在布局阶段计算的每个框转换为屏幕上的实际像素。
绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。
浏览器需要非常快地完成这项工作。
为了确保平滑滚动和动画,占据主线程的所有内容,包括计算样式,以及回流和绘制,必须让浏览器在16.67毫秒内完成。
在2048x 1536,iPad有超过314.5万像素将被绘制到屏幕上。那是很多像素需要快速绘制。
为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。如果发生这种情况,则需要进行合成。
绘制可以将布局树中的元素分解为多个层。将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重新绘制性能。
有一些特定的属性和元素可以实例化一个层,包括<video>
和<canvas>
,任何CSS属性为opacity
、3D transform
, will-change
的元素,还有一些其他元素。
这些节点将与子节点一起绘制到它们自己的层上,除非子节点由于上述一个(或多个)原因需要自己的层。
分层确实可以提高性能,但是它以内存管理为代价,因此不应作为web性能优化策略的一部分过度使用。
Compositing
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
当页面继续加载资产时,可能会发生回流(回想一下我们迟到的示例图像),回流会触发重新绘制和重新组合。
如果我们定义了图像的大小,就不需要重新绘制,只需要重新绘制需要重新绘制的层,并在必要时进行合成。
但我们没有定义图像大小!从服务器获取图像后,渲染过程将返回到布局步骤并从那里重新开始。
交互
一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。
如果加载包含JavaScript(并且延迟到onload
事件激发后执行),则主线程可能很忙,无法用于滚动、触摸和其他交互。
”Time to Interactive“(TTI)是测量从第一个请求导致DNS查找和SSL连接到页面可交互时所用的时间——可交互是”First Contentful Paint“之后的时间点,页面在50ms内响应用户的交互。
如果主线程正在解析、编译和执行JavaScript,则它不可用,因此无法及时(小于50ms)响应用户交互。
在我们的示例中,可能图像加载很快,但anotherscript.js
文件可能是2MB,而且用户的网络连接很慢。
在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。这不是一个好的用户体验。
断开TCP连接 四次挥手
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
- 客户端打算关闭连接,此时会发送一个TCP首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态。 - 服务器收到了
ACK
应答报文后,就进入了CLOSED
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSED
状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个FIN
和一个ACK
,因此通常被称为四次挥手。
这里一点需要注意是∶主动关闭连接的,才有TIME_WAIT状态。
为什么挥手需要四次?
再来回顾下四次挥手双方发FIN 包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等
服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK
和 FIN
一般都会分开发送,从而比三次握手导致多了一次。
为什么TIME_WAIT等待的时间是2MSL?
MSL
是Maximum Segment Lifetime
,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL
字段,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP
报文通知源主机。
MSL
与TTL
的区别:
MSL的单位是时间,而TTL是经过路由跳数。所以 MSL应该要大于等于TTL消耗为0的时间,以确保报文已被自然消亡。
TIME_WAIT
等待2倍的MSL,比较合理的解释是︰网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待⒉倍的时间。
如果被动关闭方没有收到断开连接的最后的ACK报文,就会触发超时重发Fin 报文,另一方接收到FIN后,会重发ACK给被动关闭方,—来一去正好2个MSL。
2MSL 的时间是从客户端接收到FIN后发送 ACK开始计时的。如果在TIME-WAIT时间内,因为客户端的ACK没有传输到服务端,客户端又接收到了服务端重发的FIN 报文,那么2MSL时间将重新计时。
为什么需要TIME_WAIT状态?
主动发起关闭连接的一方,才会有TIME-WAIT
状态。
需要TIME-WAIT状态,主要是两个原因︰
- 防止具有相同「四元组」的「旧」数据包被收到;
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的ACK能让被动关闭方接收,从而帮助其正常关闭;
经过2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
TIME-WAIT
更重要的作用是等待足够的时间以确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。
性能优化之回流重绘
回流/重排reflow
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程。
重绘Repaint
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它。
根据 Opera 的说法,重绘成本很高,因为浏览器必须验证 DOM 树中所有其他节点的可见性。
回流对性能甚至更为关键,因为它涉及影响页面一部分(或整个页面)布局的更改。元素的重排会导致所有子元素和祖先元素以及 DOM 中跟随它的任何元素的后续回流。回流在性能方面非常昂贵,并且是导致 DOM 脚本缓慢的主要原因之一,尤其是在处理能力较低的设备上,例如手机。在很多情况下,它们相当于重新布局整个页面。
不幸的是,很多事情都会引发回流。其中一些在编写 CSS 时特别相关:
- 调整窗口大小
- 更改字体
- 添加或删除样式表
- 内容更改,例如用户在输入框中键入文本
- 激活 CSS 伪类,例如 :hover(在 IE 中激活兄弟的伪类)
- 操作类属性
- 操作 DOM 的脚本
- 计算offsetWidth 和 offsetHeight
- 设置样式属性的属性
如何避免回流或至少最小化它们对性能的影响?
在 dom 树中尽可能低地更改类
当回流信息传递到周围节点时,回流可以是自上而下或自下而上的。回流是不可避免的,但您可以减少它们的影响。在 dom 树中尽可能低地更改类,从而将回流的范围限制在尽可能少的节点上。例如,您应该避免更改包装元素上的类以影响子节点的显示。面向对象的 css 总是尝试将类附加到它们影响的对象(DOM 节点或节点),但在这种情况下,它具有最小化回流影响的额外性能优势。
避免设置多个内联样式
我们都知道与 DOM 交互很慢。我们尝试将更改分组到一个不可见的 DOM 树片段中,然后当整个更改应用于 DOM 时仅导致一次重排。同样,通过 style 属性设置样式会导致重排。避免设置多个内联样式,每个样式都会导致重排,样式应该组合在一个外部类中,当操作元素的类属性时,只会导致一个重排。
应用动画到fixed或absolute的定位
将动画应用于fixed或absolute的定位元素。它们不会影响其他元素的布局,因此它们只会导致重绘而不是完全回流。这成本要低得多。
以平滑换取速度
Opera 还建议我们以平滑换取速度。他们的意思是,您可能希望一次将动画移动 1 个像素,但如果动画和随后的回流使用 100% 的 CPU,则动画会看起来很跳跃,因为浏览器难以更新流程。一次将动画元素移动 3 个像素在速度非常快的机器上可能看起来不太流畅,但在速度较慢的机器和移动设备上不会导致 CPU 抖动。
避免使用表格进行布局(或设置表格布局固定)
避免使用表格进行布局。好像您需要另一个理由来避免它们一样,表格通常需要多次传递才能完全建立布局,因为它们是元素会影响 DOM 上出现在它们之前的其他元素的显示的罕见情况之一。想象一下表格末尾的一个单元格,其内容非常宽,导致列完全调整大小。这就是为什么表格不是在所有浏览器中逐步呈现的原因,这也是为什么它们不适合布局的另一个原因。根据 Mozilla 的说法,即使是很小的更改也会导致表中所有其他节点的回流。
YUI 数据表小部件的所有者 Jenny Donnelly 建议对数据表使用固定布局,以实现更高效的布局算法。除“auto”之外的任何 table-layout 值都将触发固定布局并允许表格根据 CSS 2.1 规范逐行呈现。Quirksmode 表明浏览器对 table-layout 属性的支持在所有主要浏览器中都很好。
以这种方式,一旦接收到整个第一行,用户代理就可以开始布置表格。后续行中的单元格不影响列宽。任何具有溢出内容的单元格都使用“溢出”属性来确定是否剪切溢出内容。
该算法可能效率低下,因为它要求用户代理在确定最终布局之前可以访问表中的所有内容,并且可能需要不止一次通过。
避免在 CSS 中使用JavaScript 表达式
这条规则很老套,但很好。这些表达式如此昂贵的主要原因是因为每次文档或文档的一部分重排时都会重新计算它们。正如我们从触发回流的所有许多事情中看到的那样,它每秒可能发生数千次。
可能指CSS表达式?(例如:calc()
)
JavaScript
- 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
- 避免频繁操作DOM。
- 也可以先为元素设置
display: none
,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
React的虚拟DOM
React的虚拟DOM的作用是将真实 DOM 的副本存储在内存中。当您修改 DOM 时,它首先将这些更改应用到内存中的 DOM。然后,使用它的差异算法,找出真正发生了什么变化。最后,它对更改进行批处理,并调用一次将它们应用到 real-dom 上。因此,最大限度地减少了回流和重绘。