~~~~~如有错误,欢迎吐槽~~~~~~
前端开发中我们常常要考虑首屏加载时间,为了尽可能减少首屏加载时间我们需要弄清楚从输入网址到页面最终呈现的过程都发生了什么事情(感觉很神秘呀!!)然后才能具体问题具体分析,最终达到提升网页性能的目的。
下面我们揭秘这层神秘的面纱,从输入网址到页面呈现大致可分为以下流程:
1.网络通信
(1).用户在地址栏输入网址,浏览器进行地址解析。
(2).应用层将解析出的域名进行域名解析。
(3).传输层进行tcp三次握手,建立tcp连接。
(4).应用层客户端向web服务器发送HTTP请求。
(5).网络层IP协议查询MAC地址
(6).服务器收到处理请求。
(7).服务器发送HTTP响应报文,浏览器收到服务器响应,得到html代码
2.页面渲染
(1).解析HTML
(2).构建DOM树
(3).DOM树与CSS样式进行附着构造呈现(render)树
(4).布局
(5).绘制
看完上面的流程,宝宝还是一脸懵逼呀-><- 别怕,接下来小波为大家嘻嘻介绍~~~~
网络通信
1.用户在地址栏输入网址,浏览器进行地址解析。
我们以下面这个URL为例子,介绍下普通URL的各部分组成http://www.aspxfans.com:8080/news/index.asp?boardID=5&ID=24618&page=1#name
(1).协议部分:该URL的协议部分为“http:”,这代表网页使用的是HTTP协议。在Internet中可以使用多种协议,如HTTP,FTP等等本例中使用的是HTTP协议。在"HTTP"后面的“//”为分隔符。
(2).域名部分:该URL的域名部分为“www.aspxfans.com”。一个URL中,也可以使用IP地址作为域名使用。
(3).端口部分:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是一个URL必须的部分,如果省略端口部分,将采用默认端口。
(4).虚拟目录部分:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个URL必须的部分。本例中的虚拟目录是“/news/”。
(5).文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个URL必须的部分,如果省略该部分,则使用默认的文件名。
(6).锚部分:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个URL必须的部分。
(7).参数部分:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。
URL解析完成后我们已经手握域名,接下来进行域名解析。
2.将解析出的域名进行域名解析。
~~~《小插曲:小仙女什么是域名解析呀??就是域名到IP地址的转换过程,IP地址是网路上标识你站点的数字地址,为了简单好记,采用域名来替代IP地址。域名的解析工作由DNS服务器完成。DNS协议提供通过域名查找IP地址,或逆向从IP地址反查域名的服务》~~~
域名解析过程
(1).查找浏览器缓存,因为浏览器缓存会记录DNS记录一段时间, 有趣的是,操作系统没有告诉浏览器储存DNS记录的时间,这样不同浏览器会储存个自固定的一个时间(2分钟到30分钟不等)。
注:浏览器的DNS缓存查看和清除
a. chrome中
查看:网址栏输入:chrome://net-internals/#dns 或 输入chrome://chrome-urls/就可看到chrome所有的配置界面,然后选择chrome://dns或者chrome://net-internals/#dns之后再点击DNS就可查看。
清除:在chrome://net-internals/#dns页面中,点击“Clear host cache”,然后选择“clear cache”和“flush socket”,可以清空chrome的dns缓存。
(2).查询操作系统缓存
(3).hosts查询
windows系统在C:\Windows\Syatem32\driver\etc\hosts文件中查找;—Linux系统从/etc/hosts文件中查找。
(4).请求本地域名服务器(可以认为是你的网络接入服务器商提供,比如中国电信,中国移动,阿里云等域名供应商),如果该服务器有缓存,则直接返回,若没有,则下一步。。。一般80%到这里就可以了(比如你申请一个域名,去阿里云,那么你肯定会写上域名所指向的IP啊)
(5).从顶级域名中开始查找:根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(baidu.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找baidu.com域服务器,重复上面的动作,进行查询,直至找到www.baidu.com主机。
~~~小可爱们注意啦:上面4步中按照顺序,任何一步查找成功,后面步骤步不用再经历啦
科普时间到啦!!快过来吃瓜~~~什么是dns劫持??何可攻击根域名服务器的节点,发生在上面第四步,从DNS缓存数据库中找到时被恶意改为其他网址,所以就请求到了其他的网址。
域名解析后我们的法宝又升级为IP地址了,通过协议可以获得端口(HTTP:80,HTTP:443),下一步该进行浏览器与服务器之间的链接。
3.进行tcp三次握手,建立tcp连接(传输层)
位于传输层的TCP协议为传输报文提供可靠的字节流服务。为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。TCP协议通过“三次握手”等方法保证传输的安全可靠。
TCP建立连接过程(三次握手):
客户端发送一个TCP包。设置SYN=1(请求建立连接)、Seq=X(随机产生的序列号)
服务器发回确认包(ACK)应答。SYN=1、ACK=1、ACK number = X+1、Seq = Y(随机产生)
客户端再次发送确认包(ACK) 。SYN=0、ACK=1、ACK number= Y+1、Seq = X+1
TCP断开连接过程(四次挥手):
客户机给服务器一个FIN为1的TCP报文
服务器返回给客户端一个确认ACK报文
服务器给客户端发送一个FIN报文
客户机回复ACK报文
4.客户端向web服务器发送HTTP请求(应用层)
HTTP请求消息结构
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line),请求头部(header),空行和请求数据四部分组成。
第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
第二部分:请求头部,紧接着请求行之后的部分,用来说明服务器要使用的附加信息,包含很多有关客户端环境和请求正文的有用信息。例如:请求头可以声明浏览器所用的语言,请求正文的长度。
第三部分:空行,请求头部后面的空行是必须的,即使第四部分的请求数据为空,也必须有空行。
第四部分:请求数据也叫主体,可以添加任意的其他数据
HTTP响应消息结构
HTTP响应也由四部分组成:状态行,消息报头,空行,响应正文
第一部分:状态行,右HTTP协议版本号,状态码,状态消息三部分组成
第二部分:消息报头,用来说明客户端要使用的一些附加信息。
第三部分:空行,消息报头后面的空行是必须的。
第四部分:响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
5.网络层IP协议查询MAC地址
IP协议的作用是把TCP分割好的各种数据包传送给接收方。而要保证确实能传到接收方还需要接收方的MAC地址,也就是物理地址。IP地址和MAC地址是一一对应的关系,一个网络设备的IP地址可以更换,但是MAC地址一般是固定不变的。**ARP协议可以将IP地址解析成对应的MAC地址。**当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标。
ARP 协议是网络层的协议,但是它所工作的内容是数据链路层的。
6.服务器收到处理请求
接收端的服务器在链路层收到数据包,再层层向上直到应用层,这过程中包括在运输层通过TCP协议讲分段的数据包重新组成原来的HTTP请求报文。服务接收到客户端发送的HTTP请求后,查找客户端请求的资源,并返回响应报文,响应报文中包括一个重要的信息——状态码。状态码由三位数字组成,其中比较常见的是200 OK表示请求成功。301表示永久重定向,即请求的资源已经永久转移到新的位置。在返回301状态码的同时,响应报文也会附带重定向的url,客户端接收到后将http请求的url做相应的改变再重新发送。404 not found 表示客户端请求的资源找不到
7.服务器发送HTTP响应报文,浏览器收到服务器响应,得到html代码
服务接收到客户端发送的HTTP请求后,查找客户端请求的资源,并返回响应报文,响应报文中包括一个重要的信息——状态码。状态码由三位数字组成,其中比较常见的是200 OK表示请求成功。301表示永久重定向,即请求的资源已经永久转移到新的位置。在返回301状态码的同时,响应报文也会附带重定向的url,客户端接收到后将http请求的url做相应的改变再重新发送。404 not found 表示客户端请求的资源找不到。
接下来浏览器就要进行页面渲染了~~~
页面渲染
页面渲染基本流程:解析html以构建dom树->构建render树->布局render树-> 绘制render树
先上图:
由图可知,浏览器会解析三种东西:
(1)HTML/SVG/XHTML,解析这三种文件会产生一个DOM Tree。DOM Tree的构建是一个深度遍历的过程:当前节点的子节点都构建好之后才会构建当前节点的下一个兄弟节点。
(2)CSS,解析CSS会产生CSS树。
(3)JavaScript脚本,主要是通过DOM API 和 CSSOM API来操作 DOM Tree和 CSS Rule Tree。根据DOM树和CSSOM来构造Rendering Tree。注意啦~~Rendering Tree渲染树并不等同于DOM树,因为一些像Header或者display:none的东西就没有必要放在渲染树中了。
有了Render Tree,浏览器已经知道网页中有哪些节点,各个节点的CSS定义以及他们的从属关系,下一步操作称之为Layout,即计算尺每个节点在屏幕中的位置。最后一步就是绘制了
重点来啦:
《how browsers work》里面讲过一句话:上述这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
在网上找到下面这篇关于HTML页面加载和解析流程的文章,感觉不错,自带小板凳,过来吃瓜~~~
科普一下两个概念:
(1)Reflow(回流):浏览器要花时间去渲染,当它发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。 比如:页面初次渲染;浏览器窗口大小改变,DOM结构变化; render树变化,某些元素的尺寸,未知,内容变了;元素字体大小改变,激活CSS伪类(如:hover)。
(2)Repaint(重绘):如果只是改变了某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的repaint,重画某一部分。
回流一定伴随着重绘,而重绘却可以单独出现,Reflow要比Repaint更花费时间,也就更影响性能。所以在写代码的时候,要尽量避免过多的Reflow。
我们需要努力减少reflow/repaint,可通过下面的方式:
(1)不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。
(2)不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。
(3)为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
(4)千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
编写css时应当注意:
CSS选择符是从右到左进行匹配的。从右到左!所以,#nav li 我们以为这是一条很简单的规则,秒秒钟就能匹配到想要的元素,但是,但是,但是,是从右往左匹配啊,所以,会去找所有的li,然后再去确定它的父元素是不是#nav。因此,写css的时候需要注意:
(1)dom深度尽量浅。
(2)减少inline javascript、css的数量。
(3)使用现代合法的css属性。
(4)不要为id选择器指定类名或是标签,因为id可以唯一确定一个元素。
(5)避免后代选择符,尽量使用子选择符。原因:子元素匹配符的概率要大于后代元素匹配符。后代选择符;#tp p{} 子选择符:#tp>p{}
(6)避免使用通配符,举一个例子,.mod .hd *{font-size:14px;} 根据匹配顺序,将首先匹配通配符,也就是说先匹配出通配符,然后匹配.hd(就是要对dom树上的所有节点进行遍历他的父级元素),然后匹配.mod,这样的性能耗费可想而知.
关于Script标签:
我们大多会将script标签放在body结束标签之前,为啥呀??
(1)js代码在加载完成后,是立即执行的
(2)js在执行时会阻塞页面后续内容(包括页面的渲染、其它资源的下载)即会阻塞Dom树的构建,原因:因为浏览器需要一个稳定的DOM树结构,而JS中很有可能有代码直接改变了DOM树结构,比如使用 document.write 或 appendChild,甚至是直接使用的location.href进行跳转,浏览器为了防止出现JS修 改DOM树,需要重新构建DOM树的情况,所以 就会阻塞其他的下载和呈现。
减少JavaScript对性能的影响的方法:
1. 将所有的script标签放到页面底部,也就是body闭合标签之前,这能确保在脚本执行前页面已经完成了DOM树渲染。
2. 尽可能地合并脚本。页面中的script标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。
3. 采用无阻塞下载 JavaScript 脚本的方法:
(1)使用script标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版本); script标签的defer属性规定了是否对脚本进行延迟,直到页面加载为止。
(2)使用动态创建的script元素来下载并执行代码;
~~~~~听大神说写博客可以成为小仙女~~~~~