写这篇文章的初衷:
记得最开始学前端知识时,是一点一点的积累,一个知识点一个知识点的攻克。
就这样,虽然在很长一段时间内积累了不少的知识,但是,总是无法将它串联到一起。每次梳理时都是很分散的,无法保持思路连贯性。
直到最近,在将DNS域名解析、建立TCP连接、构建HTTP请求、浏览器渲染过程‘’流程梳理一遍后,感觉就跟打通了任督二脉一样,有了一个整体的架构,以前的知识点都连贯起来了,至少现在知道了它的大部分骨架。
梳理出一个知识体系,以后就算再学新的知识,也会尽量往这个体系上靠拢,环环相扣,更容易理解,也更不容易遗忘。这也是本文的目标。
以 前端领域 的知识为重点,并且本文内容超多,建议先了解主干,然后分批次阅读。
这篇文章真的写了好久好久…
知识体系中,最重要的是骨架,脉络。有了骨架后,才方便填充细节。所以,先梳理下主干流程:
- 浏览器接收url并开启一个新进程(这一部分可以展开浏览器的进程与线程的关系)
- 浏览器解析输入的 URL,提取出其中的协议、域名和路径等信息。(这部分涉及URL组成部分)
- 浏览器向 DNS 服务器发送请求,DNS服务器通过 多层查询 将该 域名 解析为对应的 IP地址 ,然后将请求发送到该IP地址上,与 服务器 建立连接和交换数据。(这部分涉及DNS查询)
- 浏览器与服务器建立 TCP 连接。(这部分涉及TCP三次握手/四次挥手/5层网络协议)
- 浏览器向服务器发送 HTTP 请求,包含请求头和请求体。(4,5,6,7包含http头部、响应码、报文结构、cookie等知识)
- 服务器接收并处理请求,并返回响应数据,包含状态码、响应头和响应体。
- 浏览器接收到响应数据,解析响应头和响应体,并根据状态码判断是否成功。
- 如果响应成功,浏览器接收到http数据包后的解析流程(这部分涉及到html - 词法分析,解析成DOM树,解析CSS生成CSSOM树(样式树),合并生成render渲染树(样式计算)。然后layout布局,分层,调用GPU绘制等,再显示在屏幕上。这个过程会发生回流和重绘)。
- 连接结束 -> 断开TCP连接 四次挥手
梳理出主干骨架,然后就需要往骨架上填充细节内容。
这部分内容开始之前我们需要先通过一张图对 进程 和 线程 的关系有一个初步的了解。
浏览器是多进程的,有一个主进程,每打开一个tab页面都会新开一个进程(某些情况下多个tab会合并进程)。
注意:在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,比如打开多个空白标签页。可以在Chrome任务管理器中看到,进程被合并了。
进程可能包括主进程,插件进程,GPU,tab页(浏览器内核)等等。
强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)
下图以 chrome浏览器 为例。我们可以自己通过Chrome的更多工具 =》 任务管理器 自行验证查看,可以看到chrome的任务管理器中有多个进程(分别是每一个Tab页面有一个独立的进程,以及一个主进程)
然后能看到每个进程的内存资源信息以及cpu占有率。
每一个tab页面可以看作是浏览器内核的一个进程,然后这个进程是多线程的,它有几大类子线程
GUI渲染线程与JS引擎线程是互斥的
。可以看到,里面的JS引擎是内核进程中的一个线程,这也是为什么常说JS引擎是单线程的。
后面三个只是协助,只有 JS 引擎线程是真正执行的。
JS引擎之所以是单线程,是由于JavaScript最初是作为浏览器脚本语言开发的,并且JavaScript需要操作DOM等浏览器的API,如果多个线程同时进行DOM更新等操作则可能会出现各种问题(如竞态条件、数据难以同步、复杂的锁逻辑等),因此将JS引擎设计成单线程的形式就可以避免这些问题。
虽然JS引擎是单线程的,但是通过使用 异步编程模型 和 事件循环机制,JS仍然可以实现高并发处理。
如果JS是多线程的场景描述:
那么现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?这时可能就会出现问题了。
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
因为本文主要讲输入URL后页面的渲染过程,所以关于浏览器开启网络请求线程这部分详细内容大家可以移步查看,里面包括JS运行机制,进程线程的详解:
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
JS的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JS 引擎,等 JS 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。
也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script
标签添加 defer
或者 async
属性。
关于defer/async用法解释
输入URL后,会进行解析(URL的本质就是统一资源定位符)
URL一般包括几大部分:
&
连接。#
后的hash值,一般用来定位到某个位置。举个例子,www.example.com/index.html?key1=value1&key2=value2#section 表示了一个 URL,
其中协议为 HTTP,主机名为 www.example.com,路径为 /index.html,查询参数为 key1=value1 和 key2=value2,锚点为 section。
在解析过程之前我们先理解几个概念。
DNS(Domain Name System)是一种用于将域名
解析为IP地址
的系统。(把我们的域名映射为IP地址,这就是DNS的作用)
它可以将人们易于记忆的域名转换为服务器可识别的IP地址,这样用户就可以使用域名访问网站,而不必直接输入数字格式的IP地址。
在浏览器中输入网址时,电脑会先向DNS服务器
发送请求,获取该网址对应的IP地址
,并在成功获取后直接连接该IP地址对应的服务器,在服务器端获取网页内容并显示出来,完成整个访问过程。因此,DNS在互联网中起着至关重要的作用。
IP(Internet Protocol)地址是一个数字标识,用于唯一识别连接到互联网上的每个计算机、服务器和其他设备。域名则是网站的人类可读的名称。域名系统(DNS服务器)可以将域名转换为与之关联的IP地址。
简单来说,IP地址是网络设备的标识符,而域名则是方便人们记忆和使用的网络地址别名。
域名系统通过将 域名
映射到 IP地址
,使互联网上的用户能够以易记的方式访问特定的网站或服务器。
从上面这张图可以看到,域名的管理是分层次的。最高级是根,也叫做根服务器
。从上往下功能逐渐细化。DNS就是和这些服务器进行打交道。
有了上面的这些概念,现在我们再来认识一下DNS域名解析过程就容易多了。
- 首先会在浏览器缓存中查询是否有该域名对应的IP地址,若有则直接返回,解析过程结束。
- 如果浏览器缓存中没有该域名对应的IP地址,则向本地DNS服务器发送查询请求。
- 如果本地DNS服务器缓存中有该域名对应的IP地址,则直接返回,解析过程结束。
- 如果本地DNS服务器缓存中没有该域名对应的IP地址,则向根域名服务器发送查询请求。
- 根域名服务器返回一个所查询域的顶级域名服务器地址。
- 本地DNS服务器向 顶级域名服务器 发送查询请求。
- 顶级域名服务器返回下一级DNS服务器的地址(权威DNS服务器)。
- 本地DNS服务器向权威DNS服务器发送查询请求。
- 权威DNS服务器返回该域名对应的IP地址,并将结果返回给本地DNS服务器。
- 本地DNS服务器将结果保存在缓存中,便于下次使用。并将结果返回给浏览器。
- 浏览器将结果保存在缓存中,并使用该IP地址访问对应的网站。
这个过程大体大体由一张图可以表示:从网上找的图片方便理解。
而且,需要知道dns解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch
优化
关于 本地DNS服务器 这里单独讲解下:
如果之前的过程无法解析时,操作系统会把这个域名发送给这个本地DNS服务器。每个完整的内网通常都会配置本地DNS服务器,例如用户是在学校或工作单位接入互联网,那么用户的本地DNS服务器肯定在学校或工作单位里面。它们一般都会缓存域名解析结果,当然缓存时间是受到域名的失效时间控制的。大约80%的域名解析到这里就结束了,后续的DNS迭代和递归也是由本地DNS服务器负责。
知乎上有一个阿里巴巴的回答:
从技术上来讲是可以解析到任意IP地址的,这时候针对这个地址发起HTTP访问,HTTP头中的host字段会是你的域名(而非该IP对应站点的域名),如果对方的网站HTTP服务器没有做对应的防护就可以访问,如果对方的网站HTTP服务器有防护则无法访问。
可参考:DNS解析时发现域名和IP不一致,访问了该域名会如何(大厂真题)
需要了解3次握手规则建立连接以及断开连接时的四次挥手。
拿到了IP地址后,就可以发起HTTP请求了。HTTP请求的本质就是TCP/IP的请求构建。建立连接时需要 3次握手 进行验证,断开链接也同样需要 4次挥手 进行验证,保证传输的可靠性。
模拟三次握手(场景对话版):
客户端:hello,你是server么?
服务端:hello,我是server,你是client么
客户端:yes,我是client
可通过下方图文结合方式字理解三次握手:
三次握手原理:
第一次握手:客户端发送一个带有 SYN
(synchronize同步)标志的数据包给服务端。
第二次握手:服务端接收成功后,回传一个带有 SYN/ACK
标志的数据包传递确认信息,表示我收到了。
第三次握手:客户端再回传一个带有 ACK
标志的数据包,表示我知道了,握手结束。
其中:SYN标志位数置1,表示建立TCP连接;ACK表示响应,置1时表示响应确认。
三次握手过程详细说明:
刚开始客户端处于 Closed
的状态,服务端处于 Listen
状态。
- 第一次握手: 客户端发送标识位SYN = 1,随机产生序列号seq = x的数据包到服务端,服务端由SYN = 1知道客户端要建立连接,并进入
SYN_SENT
状态,等待服务器确认;(SYN=1,seq=x,x为随机生成的数值)
- 第二次握手: 服务器收到请求并确认联机信息后,向客户端发送标识位SYN = 1,ACK = 1和随机产生的序列号seq = y, 确认码ack number = x+1(客户端发送的seq+1)的数据包,此时服务器进入
SYN_RCVD
状态;(SYN=1,ACK=1,seq=y,y为随机生成的数值,确认号 ack=x+1)
这里ack加1可以理解为时确认和谁建立连接。- 第三次握手:客户端收到后检查确认码ack number是否正确,即和第一次握手发送的序列号加1结果是否相等,以及ACK标识位是否为1;若正确,客户端发送标识位ACK = 1、seq = x + 1和确认码ack = y + 1(服务器发送的seq+1)到服务器,服务器收到后确认ACK=1和seq是否正确,若正确则完成建立连接,此包发送完毕,客户端和服务器进入
ESTAB_LISHED
状态。完成三次握手,客户端与服务器开始传送数据.。(ACK=1,seq=x+1,ack=y+1)
TCP 三次握手的建立连接的过程就是相互确认初始序号的过程。告诉对方,什么样序号的报文段能够被正确接收。
第三次握手的作用是: 客户端对服务器端的初始序列号的确认,如果只使用两次握手,那么服务器就没有办法知道自己的序号是否已被确认。同时这样也是为了防止失效的请求报文被服务器接收,而出现错误的情况。
模拟四次挥手(场景对话版):
主动方:我已经关闭了向你那边的主动通道了,只能被动接收了
被动方:收到通道关闭的信息,我这还有数据没有发送完成,你等下
被动方:那我也告诉你,我这边向你的主动通道也关闭了
主动方:最后收到数据,之后双方无法通信
四次挥手原理:
第一次挥手:客户端发送一个FIN,用来关闭客户端到服务器的数据传送,并且指定一个序列号。客户端进入FIN_WAIT_1
状态。
第二次挥手:服务器收到FIN后,发送一个ACK给客户端,确认序号为客户端的序列号值 +1 ,表明已经收到客户端的报文了,此时服务器处于 CLOSE_WAIT
状态。
第三次挥手:服务器发送一个FIN,用来关闭服务器到客户端的数据传送,服务器进入LAST_ACK
状态。
第四次挥手:客户端收到FIN后,客户端进入TIME_WAIT
状态,接着发送一个ACK给服务器,确认序号为收到序号+1 ,服务器收到确认后进入CLOSED
状态,完成四次挥手。
其中:FIN标志位数置1,表示断开TCP连接。
四次挥手过程详细说明:
刚开始双方都处于 ESTABLISHED
状态,假如是客户端先发起关闭请求。
- 第一次挥手:客户端发送一个FIN = 1、初始化序列号seq = u,到服务器,表示需要断开TCP连接,客户端进入
FIN_WAIT_1
状态,等待服务器的确认。(FIN = 1,seq = u,u由客户端随机生成)
- 第二次挥手:服务器收到这个FIN,它发回ACK = 1、seq序列号(由回复端随机生成)、确认序号ack为收到的序号加1(ack = u+1);以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。服务器进入
CLOSE_WAIT
,等待关闭连接;客户端进入FIN_WAIT_2
,稍后关闭连接。(ACK = 1,seq = v,ack = u+1)
- 第三次挥手:服务器在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开。服务器会先确保断开前,所有传输到客户端的数据是否已经传输完毕,一旦确认传输完毕,就会发回FIN = 1,ACK = 1,seq = w和确认码ack = u+1给客户端,服务器进入
LAST_ACK
状态,等待最后一次ACK确认;(FIN = 1,ACK = 1,seq = w,ack = u+1 ,w由服务器端随机生成)
- 第四次挥手:客户端收到服务器的TCP断开请求后,会回复服务器的断开请求。包含ACK = 1、随机生成的seq = u+1,并将确认序号设置为收到序号加1(ack = w+1)到服务器,从而完成服务器请求的验证回复。客户端进入
TIME-WAIT
状态,此时 TCP 未释放掉,需要等待2MSL
以确保服务器收到自己的 ACK 报文后进入CLOSE
状态,服务端进入CLOSE
状态。(ACK = 1,seq = u+1,ack = w+1)
注意:为什么 TIME_WAIT 等待的时间是 2MSL?
1)MSL 是 报文最大生存时间,一来一回需要等待 2 倍的时间。
2)最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。
常用关键词总结:
为什么需要四次挥手呢?
- TCP协议 的连接是
全双工
的,即数据传输可以同时在两个方向上进行。所以终止连接时,需要每个方向都单独关闭。(单独一方的连接关闭,只代表不能再向对方发送数据,连接处于的是半关闭状态)- 客户端发送FIN报文终止连接后,
服务器可能还有数据需要发送
(比如上一次的响应),所以服务器会先发送ACK报文确认收到FIN报文,并将未发送的数据发送出去,然后再发送自己的FIN报文终止连接。- 客户端接收到服务器的FIN报文后也需要发送ACK报文确认收到,才能正式关闭连接。
为了确认双方的 接收能力 和 发送能力 都正常。
如果是用两次握手,则会出现下面这种情况:
如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,此时客户端共发出了两个连接请求报文段。
其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络节点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误以为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手;只要服务端发出确认,就建立新的连接了。此时客户端忽略服务端发来的确认,也不发送数据,则服务端一直等待客户端发送数据,浪费了资源。
这个概念挺难记全的,这里先对它有个整体概念就好。
其实就是一个概念:从客户端发出HTTP请求到服务器接收,中间会经过一系列的流程。
简括就是:
从应用层发送HTTP请求,到传输层通过三次握手建立tcp/ip连接,再到网络层的ip寻址,再到数据链路层的封装成帧,最后到物理层的利用物理介质传输。
当然,服务端的接收就是反过来的步骤。
五层因特网协议栈其实就是:(由上往下)
- 应用层(DNS,HTTP)DNS将域名解析成IP地址并发送HTTP请求,OSI 参考模型中最靠近用户的一层。
- 传输层(TCP,UDP) 建立TCP连接(三次握手),客户端和服务端数据传输就是在这层进行的。
- 网络层(IP,ARP地址解析协议)IP寻址及路由选择
- 数据链路层:封装成帧
- 物理层(利用物理介质传输比特流) 物理传输(然后传输的时候通过双绞线,电磁波等各种介质)
其实也有一个完整的OSI七层框架,与之相比,多了会话层、表示层。
OSI七层框架:从上到下分别是 应用层
、表示层
、会话层
、传输层
、网络层
、数据链路层
、物理层
表示层:主要处理两个通信系统中交换信息的表示方式,包括数据格式交换,数据加密与解密,数据压缩与终端类型转换等。
会话层:它具体管理不同用户和进程之间的对话,如控制登陆和注销过程
OSI七层框架,它的缺点是分层太多,增加了网络工作的复杂性,所以没有大规模应用。后来人们对 OSI 进行了简化,合并了一些层,最终只保留了 4 层
1.应用层(会话层,表示层,应用层)
2.传输层
3.网络层
4.网络接口层 (数据链路层,物理层)
引用网上的图例解释:
HTTP请求报文主要由三个部分组成:请求行
、请求头
和请求体
。具体如下:
请求行:包含请求方法
、URI(请求的资源路径)
和HTTP协议版本
。例如:GET /index.html HTTP/1.1。
请求头(Header): 包含了客户端向服务器发送的附加信息,例如浏览器类型、字符编码、认证信息等。请求头以键值对
的形式存在,多个键值对之间以换行符分隔。例如:Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7。
请求体(Body): 存放请求参数
,即浏览器向服务器传输数据的实体部分。常用于POST方法提交请求时,发送表单数据、JSON数据等类型的数据。
需要注意的是,并不是所有的HTTP请求都必须带有请求体,像GET请求
通常不需要发送请求体。
为什么 HTTP 报文中要存在 “空行”?
因为 HTTP 协议并没有规定报头部分的键值对有多少个。空行就相当于是 “报头的结束标记”, 或者是 “报头和正文之间的分隔符”。
HTTP 在传输层依赖 TCP 协议, TCP 是面向字节流的. 如果没有这个空行, 就会出现 “粘包问题”
区分状态码
1××开头 - 信息性状态码,表示HTTP请求已被接收,需要进一步处理。
2××开头 - 成功状态码,表示请求已成功处理完成。
3××开头 - 重定向状态码,表示请求需要进一步的操作以完成。
4××开头 - 客户端错误状态码,表示请求包含错误或无法完成。
5××开头 - 服务器错误状态码,表示服务器无法完成有效的请求。
常见状态码
200 - 请求成功,从客户端发送给服务器的请求被正常处理并返回
301 - 表示被请求的资源已经被永久移动到新的URI(永久重定向)
302 - 表示被请求的资源已经被临时移动到新的URI(临时重定向)
304 - 表示服务器资源未被修改;通常是在客户端发出了一个条件请求,服务器通过比较资源的修改时间来确定资源是否已被修改
400 - 服务器不理解请求,请求报文中存在语法错误
401 - 请求需要身份验证
403 - 服务器拒绝请求(访问权限出现问题)
404 - 被请求的资源不存在
405 - 不允许的HTTP请求方法,意味着正在使用的HTTP请求方法不被服务器允许
500 - 服务器内部错误,无法完成请求
503 - 服务器当前无法处理请求,一般是因为过载或维护
请求和响应头部也是分析时常用到的。
常用的请求头部(部分):
Accept: 接收类型,表示浏览器支持的MIME类型
(对标服务端返回的Content-Type
)
Accept-Encoding:浏览器支持的压缩类型,如gzip
等,超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
If-Modified-Since:对应服务端的Last-Modified
,用来匹配看文件是否变动,只能精确到1s之内,http1.0
中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
If-None-Match:对应服务端的ETag
,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie
并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin
比Referer
更尊重隐私
Referer:该页面的来源URL
(适用于所有类型的请求,会精确到详细页面地址,csrf
拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等
常用的响应头部(部分):
Access-Control-Allow-Headers: 服务器端允许的请求
Headers
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求Origin
头部(譬如为*)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期,从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control
后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的cookie
,服务器通过这个头部把cookie
传给客户端
Keep-Alive:如果客户端有keep-alive
,服务端也会有响应(如timeout=38)
Server:服务器的一些相关信息
一般来说,请求头部和响应头部是匹配分析的。
譬如,请求头部的Accept
要和响应头部的Content-Type
匹配,否则会报错。
譬如,跨域请求时,请求头部的Origin
要匹配响应头部的Access-Control-Allow-Origin
,否则会报跨域错误。
譬如,在使用缓存时,请求头部的If-Modified-Since
、If-None-Match
分别和响应头部的Last-Modified
、ETag
对应。
注意点
:
请求头 和 响应头 中的 Content-Type
,是不一样的。
请求头的Content-Type常见取值:
application/x-www-from-urlencoded //以键值对的数据格式提交
multipart/form-data //用于上传文件图片等二进制数据
响应头的Content-Type常见取值:
text/html // body 数据格式是 HTML
text/css // body 数据格式是 CSS
application/javascript // body 数据格式是 JavaScript
application/json //body 数据格式是 JSON (最常见的)
http 请求 时,除了头部,还有消息实体
,一般来说,
请求实体中会将一些需要的参数都放入(用于post
请求)。
比如实体中可以放参数的序列化形式(a=1&b=2
这种),或者直接放表单对象(Form Data
对象,上传时可以夹杂参数以及文件)等等。
而一般 响应实体中,就是放服务端需要返给客户端的内容。
一般现在的接口请求时,实体中就是信息的json格式,而像页面请求这种,里面直接放了一个html字符串,然后浏览器自己解析并渲染。
如下图所示(post请求发送给接口的数据)
注意点
:
GET请求
通常不需要发送请求体。不一定。这时候要判断Connection字段, 如果请求头或响应头中包含
Connection: Keep-Alive
,
表示建立了持久连接
,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接, 请求-响应流程结束。
cookie是浏览器的一种本地存储方式,一般用来帮助 客户端 和 服务端 通信的,常用来进行身份校验,结合服务端的 session 使用。
场景如下(简述):
在登陆页面,用户登陆了
此时,服务端会生成一个session
,session
中有对应用户的信息(如用户名、密码等)
然后会有一个sessionid
(相当于是服务端的这个session对应的key)
然后服务端在登录页面中写入cookie
,值就是: jsessionid=xxx
然后浏览器本地就有这个cookie
了,以后访问同域名
下的页面时,自动带上cookie,自动检验,在有效时间内无需二次登陆。
一般来说,cookie是不允许存放敏感信息的(千万不要明文存储用户名、密码),因为非常不安全,如果一定要强行存储,首先,一定要在cookie中设置httponly
(这样就无法通过js操作了),另外可以考虑RSA等非对称加密(因为实际上,浏览器本地也是容易被攻克的,并不安全)
另外,由于在同域名的资源请求时,浏览器会默认带上本地的cookie,针对这种情况,在某些场景下是需要优化的。
比如以下场景:
客户端在 域名A 下有cookie(这个可以是登录时由服务端写入的)
然后在 域名A 下有一个页面,页面中有很多依赖的静态资源(都是 域名A 的,譬如有20个静态资源)
此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上 cookie
也就是说,这20个静态资源的 http请求,每一个都得带上 cookie,而实际上静态资源并不需要 cookie 验证
此时就造成了较为严重的浪费,而且也降低了访问速度(因为内容更多了)
针对这种场景,是有优化方案的(多域名拆分
)。具体做法就是:
static.base.com
)page.base.com
(页面所在域名)下请求时,是不会带上 static.base.com
域名的cookie的,所以就避免了浪费说到了多域名拆分,这里再提一个问题,那就是:
dns-prefetch
(让浏览器空闲时提前解析dns域名,不过也请合理使用,勿滥用)HTTP协议的版本历经多次更新迭代,主要包括 HTTP/1.0
、HTTP/1.1
和HTTP/2
等版本,它们之间的主要区别如下:
1)HTTP/1.0:
短连接
,浏览器的每次请求都需要与服务器建立一个TCP连接,都要经过三次握手,四次挥手。而且是串行请求。“队头阻塞”
。2)HTTP/1.1:目前使用最广泛的版本
长连接
,通过Connection: keep-alive
保持HTTP连接不断开,避免重复建立TCP连接。管道化传输
,通过长连接实现一个TCP连接中同时处理多个HTTP请求;只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。断点续传
, 新增 Range 和 Content-Range 头表示请求和响应的部分内容Host
字段;为了支持多虚拟主机的场景,使用同一个IP地址上可以托管多个域名,访问的都是同一个服务器,从而满足HTTP协议发展所需要的更高级的特性。缺点:
3)HTTP/2.0:
二进制格式
而非文本格式多路复用
,在同一个TCP连接上同时传输多条消息;每个请求和响应都被分配了唯一的标识符,称为“流(Stream)”,这样每条信息就可以独立地在网络上传输。报头压缩
,降低开销。服务器推送
,支持服务器主动将相关资源预测性地推送给客户端,以减少后续的请求和延迟。(例如 HTML、CSS、JavaScript、图像和视频等文件)4)HTTP3.0
是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。
- 运输层由TCP改成使用UDP传输
- 队头堵塞问题的解决更为彻底
- 切换网络时的连接保持:基于TCP的协议,由于切换网络之后,IP会改变,因而之前的连接不可能继续保持。而基于UDP的QUIC协议,则可以内建与TCP中不同的连接标识方法,从而在网络完成切换之后,恢复之前与服务器的连接
- 升级新的压缩算法
注意: HTTP 1.1起支持长连接,keep-alive不会永远保持,它有一个持续时间,一般在服务器中配置(如apache),另外长连接需要客户端和服务器都支持时才有效。
管道传输和多路复用的区别
HTTP/2 的多路复用可以理解为一条公路上同时行驶多辆车的场景,每辆车对应一个请求或响应,而公路对应一个 TCP 连接。
在 HTTP/1.x 中,只能一辆车(请求或响应)通过这条公路,其他车必须等待前面的车通过后再行驶;
而在 HTTP/2 中,则允许多辆车同时在这条公路上行驶,它们之间不会互相干扰或阻塞,从而提高了公路的使用效率和通行能力。
关于HTTP协议这部分感兴趣的可以看 HTTP的前世今生
浏览器缓存的特点:
先在浏览器缓存中查找该请求的结果以及缓存标识
将该结果和缓存标识存入浏览器缓存中
根据是否需要向服务器重新发起HTTP请求将缓存过程分为两个部分,分别是强缓存
和协商缓存
。
过期时间
内,是的话直接从本地缓存中读取资源,不与服务器进行通信。常见的缓存控制字段有Expires
和Cache-Control
。注意,如果同时启用了Cache-Control与Expires,Cache-Control优先级高。资源没有更新
,则返回状态码 304
Not Modified,告诉浏览器可以使用本地缓存;否则返回新的资源内容。强缓存优先级高于协商缓存
,但是协商缓存可以更加灵活地控制缓存的有效性。浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
1)解析HTML: 解析
HTML
并构建DOM
树。
2)解析CSS: 解析CSS
构建CSSOM
树(样式树)。
3)合成渲染树:将DOM
与CSSOM
合并成一个渲染树
(Render Tree) 。
4)布局计算:根据渲染树的结构,计算每个节点
在屏幕上的大小
、位置
等属性,生成布局信息(Layout)。这个过程会发生回流和重绘。
5)绘制页面:将生成的布局信息交给浏览器的绘图引擎,通过GPU
加速将像素绘制(Paint)到屏幕上。
6)浏览器回流和重绘:如果页面发生改变,浏览器需要重新计算布局和绘制,这可能会导致性能问题。因此我们应尽量避免频繁的 DOM 操作和调整元素样式,以减少不必要的回流和重绘。
解析过程中遇到 CSS 解析 CSS,遇到 JS 执行 JS。为了提高解析效率,浏览器在开始解析前,会启动一个预解析的线程,率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。
如果主线程解析到link
位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
如果主线程解析到script
位置,会停止解析 HTML,转而等待 JS 文件下载好,并将全局代码解析执行完成后,才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树,所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。
浏览器会遵守一套步骤将 HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:
浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如UTF-8)将它们转换成字符串。
在网络中传输的内容其实都是0和1这些字节数据。当浏览器接收到这些字节数据以后,它会将这些数据转换为字符串,就是我们的代码。
比如假设有这样一个HTML页面:(以下部分的内容出自参考来源,修改了下格式)
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Pathtitle>
head>
<body>
<p>Hello <span>web performancespan> students!p>
<div><img src="awesome-photo.jpg">div>
body>
html>
浏览器的处理过程如下:(以下图片出自参考来源)
注意点:
- 将字符串转换成Token,例如:
、
、
等。Token中会标识出当前Token是 “开始标签” 或是 “结束标签” 亦或是 “文本” 等信息。
- 构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token,一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会再去创建节点对象。
构建 CSSOM 树的过程与 构建DOM 的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后 构建节点 并生成 CSSOM。简述为:
这一过程中,CSS匹配HTML元素是一个相当复杂和有性能问题的事情,浏览器得递归CSSOM树,确定每一个节点的样式到底是什么,所以DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为 渲染树。
(以下图片出自参考来源)
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display: none
的,那么就不会在渲染树中显示。
主线程会遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。
在这一过程中,很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
这一步完成后,会得到一棵带有样式的 DOM 树。
布局完成后会生成布局树。
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做 回流
)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切 位置和大小
。通常这一行为也被称为“自动重排”。
布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局阶段会依次遍历 DOM 树的每一个节点,计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。
大部分时候,DOM 树和布局树并非一一对应。
比如display:none
的节点没有几何信息,因此不会生成到布局树;又比如使用了伪元素选择器
,虽然 DOM 树中不存在这些伪元素节点,但它们拥有几何信息,所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。
主线程会使用一套复杂的策略对整个布局树中进行分层。
分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率。
滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果,也可以通过will-change
属性更大程度的影响分层结果。
关于分层我们可以f12查看layers这一项,没有的话,就去浏览器更多工具里打开。
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。
完成绘制后,主线程将每个图层的绘制信息提交给合成线程,剩余工作将由合成线程完成。
合成线程首先对每个图层进行分块,将其划分为更多的小区域。
它会从线程池中拿取多个线程来完成分块工作。
(1)回流
回流 的本质就是重新计算 layout 树
。
当进行了会影响布局树的操作后,需要重新计算布局树,会引发 layout。
为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当 JS 代码全部完成后再进行统一计算。所以,改动属性造成的 回流 是异步
完成的。
也同样因为如此,当 JS 获取布局属性时,就可能造成无法获取到最新的布局信息。
浏览器在反复权衡下,最终决定获取属性(比如 dom.clientWidth)立即 回流。
(2)重绘
重绘 的本质就是重新根据分层信息计算了绘制
指令。
当改动了可见样式后,就需要重新计算,会引发 重绘。
由于元素的布局信息也属于可见样式,所以 回流 一定会引起 重绘。
(3)最后总结
回流必定会发生重绘,重绘却可以单独出现
。回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
(4)什么情况引起回流?
- 页面的首次渲染
- 浏览器的窗口大小发生变化
- 元素内容发生变化
- 元素的尺寸或位置发生变化
- 元素的字体大小发生变化
- 添加或删除可见的DOM元素
- 激活CSS伪类
- 查询某些属性或者调用某些方法
所以一般会有一些优化方案,如:
操作DOM时,尽量在低层级的DOM节点进行操作
不要使用 table
布局,一个小的改动可能会使整个 table 进行重新布
不要频繁操作元素的样式,对于静态页面,可以修改类名,而不是样式
使用absolute
或者fixed
,使元素脱离文档流,这样他们发生变化就不会影响到其他元素
频繁操作DOM,使用文档片段(DocumentFragment):当需要在 DOM 中插入大量节点时,可以先将这些节点放入文档片段中,然后再将整个文档片段一次性插入到 DOM 中。这样可以减少回流和重绘的次数。
将DOM 的多个读操作(或者写操作)放在一起,而不是读写穿插着写。这得益于浏览器的渲染队列机制
。浏览器会将所有的回流、重绘放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器会对队列进行批处理,将多次的回流、重绘变成一次
对于频繁读取但不经常改变的页面元素,可以使用 CSS 动画实现动态效果,避免使用 JS 频繁操作 DOM。
注意:改变字体大小会引发回流。
浏览器渲染小结:
整个过程如下:
DOM TREE(DOMContentLoaded事件触发) => 「执行JS」没完成会阻止接下来的渲染 => CSSOM TREE => RENDER TREE渲染树「浏览器未来是按照这个树来绘制页面的」=> Layout布局计算「回流/重排」=> Painting绘制「重绘」{ 分层绘制 }
需要注意几个事项:
1. CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。(这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建)
2. 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至JS脚本下载完成并执行后才会继续解析HTML。因为 JavaScript 可以使用诸如 document.write() 更改整个 DOM 结构之类的东西来更改文档的形状,因此 HTML 解析器必须等待 JavaScript 运行才能恢复HTML文档解析。
3. 如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,建议将 script 标签放在 body 标签底部。
4. 如果主线程解析到link
位置,此时外部的 CSS 文件还没有下载解析好,主线程不会等待,继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。
本文的目的:梳理出自己的知识体系。
梳理出知识体系后,有了一个大致骨架,由于知识点是环环相扣的,后期也不容易遗忘。以后就算在这方面又学习了新的知识,有了这些基础学起来也会事半功倍些。更重要的是容易举一反三,可以由一个普通问题,深挖拓展到底层原理。
以后再有相关问题,也会继续在这个骨架上填充细节。
可参考:
从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!
超详细讲解页面加载过程
浏览器渲染
前端知识体系整理 - 浏览器页面加载过程