从输入URL到浏览器显示页面发生了什么(前端篇)

为什么要强调前端篇,因为看到有一部分技术文章更是讲到了硬件方面的知识,因为自己目前能力有限,所以更着重于前端这一块。

到现在也看了许多的技术文章了,今天第一次下笔写文章,主要是为了加深自己这方面的印象,二来指不定在自己写的过程中会遇到一些自己以前没有注意过的新问题。

当用户在网址栏输入信息的时候,当用户输入url时,浏览器会从本地地址中调取缓存的历史记录,来减少用户需要输入的文字。还有一种情况是输入非url时,类似于chrome浏览器,它会将你输入的字符进行转换为非ASCII码的Unicode字符作为后缀添加在url后面进行搜索。

由于用户更擅于记住域名和主机名,而计算机更擅于处理一组纯数字的IP地址,这时DNS服务则应运而生。DNS协议提供通过域名查找IP地址或逆向IP地址反查找域名的服务。

当用户确认输入好之后信息后,点击回车确认,浏览器首先会从本地DNS缓存(chrome://net-internals/#dns)中读取当前网址所对应的域名;如果缓存中没有找到,则去本机中的gethostbyname库函数进行查询(不同操作系统中的函数不同),会在本机host文件中查看是否有对应的域名;如果gethostbyname库没有找到,则会向本地DNS服务器发一条DNS查询请求,通常是在缓存在本地路由器或运营商的服务器;如果还没有找到,则会一直向上寻找从运营商服务器到全球最顶部13台dns根域名服务器,然后依次返回到本机。(好像亚洲唯一一台在日本,不知道是不是几年第一个浏览器页面在日本诞生,瞎猜猜)

由于早期的 DNS 查询结果是一个512字节的 UDP 数据包。这个包最多可以容纳13个服务器的地址,因此就规定全世界有13个根域名服务器,编号从a.root-servers.net一直到m.root-servers.net。但是为了保证根域名服务器的可用性,服务器运营商会部署多个节点。所以,根域名服务器其实不止13台。据统计,截止2016年1月,全世界共有 517 台根域名服务器


当用户获得域名之后,主机开始发送TCP/IP请求,即经典的三次握手。

第一次握手。第一次向服务器发送码SYN=1,随机产生一个seqnumber(sequence number)码(1234567),表示请求建立连接;

第二次握手。当服务器接收到确认联机的信息后,向主机发送一个ACKnumber=(主机的seqnumber+1),SYN(synchorinize)=1,ACK(acknowledgment)=1,并随机产生一个seqnumber=(7654321)的包。

第三次握手。主机会检查收到acknumber是否正确,即是否为第一次发送的seqnumber+1,一级ACK码是否为1,主机如果收到SYN和ACK都为1则表示确认连接。

完成三次握手,主机开始与服务器发送数据。如果在这三次握手中任何一个环节出了问题,TCP协议会再次以相同的顺序发送数据包。当然除了3次握手,TCP协议还有其他各种手段保证通信的可靠性。

这里涉及到一个知识点,就是为什么要三次而不是两次握手。看过几篇文章的表述,用自己理解的语言来说下。关键是在于第三次请求,假设第三次请求因为某些原因没有及时发送到服务端,而服务端再过了一段时间之后才获取到这个延迟请求,会将此误认为一个新的连接请求,但实际上这并不是一个新的运输连接。结果服务器苦苦等着客户端不可能发送过的数据,从而浪费了许多服务器资源。而如果有了三次握手,服务端如果不收到第三次请求,就知道客户端不会发送请求。

当文件传输完毕后,浏览器开始渲染页面。这里浏览器有一个渲染引擎,即我们常说的各种浏览器内核,比如Safari的webkit,chrome的Blink,IE的edge,另外一个则是js引擎,例如有chrome的V8引擎,IE的EdgeJScript,FireFox的SpiderMonkey(firefox的第一款引擎世界的第一款引擎,js之父所写)。


服务器和客户端都可以发出中断请求

服务器发出中断请求情况

当一个请求发完之后,则开始了四次挥手。
第一次挥手:由服务器先发送了一个FIN码,随机产生一个序号i,用来关闭主动方到被动方的传输,告诉主机我不会再给你发送数据了(当然在FIN码发送之前发出去的数据,如果没有收到对应的ACK确认报文,则依然会重发这些数据)。此时主机仍然在可以接受数据

第二次挥手:主机收到FIN码之后,发出一个ACK给服务器,确认序号为收到序号i+1(与SYN相同,一个FIN占用一个序号)

第三次挥手:被动关闭方发送一个FIN码,用来关闭被动方和主动方的数据连接,也就是告诉主动关闭方,我的数据已经发送完了,不会再给你主动发送数据了。

第四次挥手:主动关闭方收到FIN码之后,发送一个ACK给被动关闭方,确认序号为收到序号i+1。至此,完成4次挥手

用我自己的理解来说就是,

  • 前两次是互相确认没有数据传输了(1.a告诉b没有数据给你;2.b知道a没数据给自己了;),
  • 后两次是确认不需要连接了(3.a告诉b关闭连接了;4.b知道a关闭连接了,a接受到反馈知道b也关闭连接了)。
客户端发出中断请求情况

(1) 客户端发出一个FIN码,产生一个序号i用来关闭主动到被动方的数据连接,
(2) 服务器收到一个FIN码后,它发回一个ACK,确认序号为收到的序号i+1,
(3) 服务器关闭了被动关闭方和主动关闭方的数据连接,并发送一个FIN码给主动关闭方
(4) 客户端收到确认的ACK报文后,并将确认序号设置为收到序号i+1

这里有一个问题在于为什么挥手比牵手多了一次动作。我的理解就是多了一个关闭连接的操作。互相不发送数据是半关闭,互相关闭连接是真正关闭。


页面渲染

首先开始根据html文件开始构建最初的DOM树,然后根据获取的css文件的样式表,形成CSSOM,这时候结合dom和CSSOM,创建出来一颗渲染树。在渲染树中,每一个字符串都是一个独立的渲染节点,每一个渲染节点都是经过计算的,叫做布局(“layout”)。如果此时获取到js文件并开始执行时,渲染过程则会停止(渲染和执行js不能同时执行)。在js执行过程中,会有相关代码会导致浏览器的重绘(repaint)和回流(reflow),这是首屏渲染应该注意的地方。

引起回流的js代码

众所周知,现在许多提高页面性能的方法都是有一条都是减少dom的操作,而dom操作影响的直接结果一般都是说dom的变化引起了dom结构的改变,导致了页面的重排,这个引起的过程可以叫做回流。引起回流的情况有许多,列几项典型举例下:

  • dom元素本身的大小改变(原因有width,内容,margin,padding,等等的变化)影响了页面结构,直接导致其下面所有元素的重排。
  • 获取特殊属性如offsetLeft(家族),scrollLeft(家族),clientHeight(家族),padding等(原因在于浏览器为了获得正确的值,会强制触发一次重排),所以应用临时变量缓存起来
  • 添加和删除DOM元素
  • 页面窗口的变化,也就是resize

常见的优化方法网上也是一搜一大堆,不过大多数方法核心点是在于尽可能减少直接影响的次数,列几项典型举例下:

  • dom元素脱离文档流,比如absolute什么的
  • 将多次dom增删实现存储虚拟dom中,然后最后一次直接操作dom树;或者多个样式合在一个class变为一次操作。
  • 类似于上条,先display:none隐藏dom元素操作,再出现,只操作两次
  • 存储会获取会引起重排的属性,缓存到一个变量中

幸运的是现代浏览器引擎有时候会积累足够的变化,或一定时间,或一个线程结束才发生一次变化。不过好的规范应该在编写代码时就注意到。


咦,好像写完了,感觉不会那么少啊,是不是我还漏了很多和前端有关的东西忘记写啊