chwzmx 2019-11-08
为什么输入URL就可以显示想要的页面?浏览器偷偷做了些什么?前端可以针对这些做哪些优化?
最近,在准备面试,这个问题我是被面试官问怕了,浏览器到底做了什么?你去问浏览器呗(尴尬)。想一下还是系统学习总结一下吧,话不多说看张图!
可以看出来,从输入URL到浏览器显示完成可以大概分为5个阶段:
浏览器在发起http请求前,会先解析这个域名,找到ip地址。这个过程就是dns解析。解析过程如下:
1)浏览器先查询hosts文件是否有与这个域名对应的ip地址,如果有则直接向这个ip地址发起http请求。查询不到就进行下一步。
2)浏览器向本地DNS服务器发出解析域名的DNS解析报文,本地DNS服务器收到请求后,先查询缓存,判断是否有对应的记录,如果有就返回这条记录,查询不到就进行下一步。
3)本地DNS服务器没有在缓存中查询到对应的记录,本地DNS服务器于是就向DNS根服务器发起查询请求。DNS根服务器收到请求通过查询得到顶级域名对应的顶级域服务器的ip地址,然后向本地DNS服务器发送一条应答报文。
4)本地DNS服务器收到应答报文后,得到顶级域服务器的地址,然后向该地址发送请求解析域名的DNS请求报文。
5)顶级域名服务器在收到请求后先查询缓存是否有对应的记录,如果有就返回对应的记录,如果没有找到就查询域名对应的二级域服务器地址,然后将域名对应的二级域服务器地址返回给本地DNS服务器。
6)本地DNS服务器收到应答报文后,得到二级域服务器的地址,然后向该地址发送请求解析域名的DNS请求报文。
7)二级域服务器在收到请求后先查询缓存是否有对应的记录,如果有就返回对应的记录,如果没有找到就查询域名对应的三级域服务器地址,然后将域名对应的三级域服务器地址返回给本地DNS服务器。
8)本地DNS服务器收到应答报文后,得到三级域服务器的地址,然后向该地址发送请求解析域名的DNS请求报文。
9)三级域服务器在收到请求后在DNS区域数据库中查询对应的记录,返回对应的记录
10)本地名称服务器在收到三级域服务器后,向用户返回一条DNS应答报文,并将这条记录保存在缓存中
11)浏览器就得到了域名对应的ip地址,然后就可以发起http请求了
看一看下面的3个概念: ACK : TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1 SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。 FIN (finis):完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。
上面介绍了tcp的3次握手和4次挥手过程,在进行http连接前需要通过tcp的3次握手来让客户端和服务端互相确认彼此,才能安心的进行数据传输。
经过上面复杂的沟通,双端终于可以说话了,通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。在浏览器查看一个http请部如下,包括3个部分:
1)通用头域包含请求和响应消息都支持的头域。 2)Request URL:请求的URL地址 3)Request Method: 请求方法,get/post/put/…… 4)Status Code:状态码,200 为请求成功 5)Remote Address:路由地址
1) Accept: 告诉WEB服务器自己接受什么介质类型,*/* 表示任何类型,type/* 表示该类型下的所有子类型; 2)Accept-Charset: 浏览器申明自己接收的字符集 Accept-Encoding:浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法 (gzip,deflate) 3)Accept-Language: 浏览器申明自己接收的语言。语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等。 4)Authorization: 当客户端接收到来自WEB服务器的 WWW-Authenticate 响应时,该头部来回应自己的身份验证信息给WEB服务器。 5)Connection:表示是否需要持久连接。close(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接, 不要等待本次连接的后续请求了)。keep-alive(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。 6)Referer:发送请求页面URL。浏览器向 WEB 服务器表明自己是从哪个 网页/URL 获得/点击 当前请求中的网址/URL。 7)User-Agent: 浏览器表明自己的身份(是哪种浏览器)。 8)Host: 发送请求页面所在域。 9)Cache-Control:浏览器应遵循的缓存机制。 no-cache(不要缓存的实体,要求现在从WEB服务器去取) max-age:(只接受 Age 值小于 max-age 值,并且没有过期的对象) max-stale:(可以接受过去的对象,但是过期时间必须小于 max-stale 值) min-fresh:(接受其新鲜生命期大于其当前 Age 跟 min-fresh 值之和的缓存对象) 10)Pramga:主要使用 Pramga: no-cache,相当于 Cache-Control: no-cache。 11)Range:浏览器(比如 Flashget 多线程下载时)告诉 WEB 服务器自己想取对象的哪部分。 12)Form:一种请求头标,给定控制用户代理的人工用户的电子邮件地址。 13)Cookie:这是最重要的请求头信息之一
1)Age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。 2)Accept-Ranges:WEB服务器表明自己是否接受获取其某个实体的一部分(比如文件的一部分)的请求。bytes:表示接受,none:表示不接受。 3) Cache-Control:服务器应遵循的缓存机制。 public(可以用 Cached 内容回应任何用户) private(只能用缓存内容回应先前请求该内容的那个用户) no-cache(可以缓存,但是只有在跟WEB服务器验证了其有效后,才能返回给客户端) max-age:(本响应包含的对象的过期时间) ALL: no-store(不允许缓存) 4) Connection: 是否需要持久连接 close(连接已经关闭)。 keepalive(连接保持着,在等待本次连接的后续请求)。 Keep-Alive:如果浏览器请求保持连接,则该头部表明希望 WEB 服务器保持连接多长时间(秒)。例如:Keep-Alive:300 5)Content-Encoding:WEB服务器表明自己使用了什么压缩方法(gzip,deflate)压缩响应中的对象。 例如:Content-Encoding:gzip 6)Content-Language:WEB 服务器告诉浏览器自己响应的对象的语言。 7)Content-Length:WEB 服务器告诉浏览器自己响应的对象的长度。例如:Content-Length: 26012 8)Content-Range:WEB 服务器表明该响应包含的部分对象为整个对象的哪个部分。例如:Content-Range: bytes 21010-47021/47022 9)Content-Type:WEB 服务器告诉浏览器自己响应的对象的类型。例如:Content-Type:application/xml 10)Expired:WEB服务器表明该实体将在什么时候过期,对于过期了的对象,只有在跟WEB服务器验证了其有效性后,才能用来响应客户请求。 11) Last-Modified:WEB 服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。 12) Location:WEB 服务器告诉浏览器,试图访问的对象已经被移到别的位置了,到该头部指定的位置去取。 13)Proxy-Authenticate: 代理服务器响应浏览器,要求其提供代理身份验证信息。 14)Server: WEB 服务器表明自己是什么软件及版本等信息。 15)Refresh:表示浏览器应该在多少时间之后刷新文档,以秒计。
浏览器是一个边解析边构建渲染树的过程,渲染引擎会尝试尽快的把内容显示出来。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在处理后续内容的同时把处理过的局部内容先展示出来。
首先浏览器解析HTML文件构建DOM树,Parser模块主要负责解析HTML页面,完成从HTML文本到HTML语法树再到文档对象树(Document Object Model Tree,DOM Tree)的映射过程。
解析到CSS文件时则会下载css构建渲染树。遇到image等资源也会同步下载不会阻塞html树的构建。但是由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头重新渲染这部分代码(重排/重绘)。
CSS解析的过程类似于HTML解析,也是浏览器使用自带的解析器进行解析,一般解析过程是由上而下,会将CSS文件解析成为StyleSheet对象,且每个对象都包含CSS规则。CSS规则对象包含了选择和声明对象,以及其他与CSS语法对应的对象。CSS解析完成后会大致生成如下结构的CSS Rule Tree。
css解析过程:
遇到js时情况就变的复杂了。当遇到script标签的时候浏览器暂停解析(不是暂停下载),将控制权交给JavaScript引擎(解释器)。因为js中会存在对dom的操作,操作完成后会触发 重排/重绘 ,所以继续解析也没有意义。
如果<script>是内联脚本,则直接执行js。下面方法放在页面内会导致页面空白6秒。
function time(){ var n = Number(new Date()); var n2 = Number(new Date()); while((n2 - n) < (6*1000)){ n2 = Number(new Date()); } }
如果<script>标签引用了外部脚本,就下载该脚本,否则就直接执行,执行完毕后将控制权交给浏览器渲染引擎。上面方法放在外部js脚本内,则是解析到script标签时,html解析停止6秒。
CSS文件的加载不影响JS文件的加载,但是却影响JS文件的执行。JS代码执行前浏览器必须保证CSS文件已经下载并加载完毕。
看下图html解析过程:
script提供了2个异步加载js属性:
async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。 下载时不会阻塞dom解析,执行时阻塞。 defer:如果script标签设置了该属性,则浏览器会开启新的线程下载脚本文件, 并且不会影响到后续DOM的渲染; 如果有多个设置了defer的script标签存在,则会按照顺序执行所有的script; defer脚本会在文档渲染完毕后,DOMContentLoaded事件调用前执行。
DomContentLoaded 事件只关注 HTML 是否被解析完,而不关注 async 脚本。
如果 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到 HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。
渲染的主要过程分为——Render Tree(渲染树)生成——Layout(布局)——Paint(绘制)。
DOM树和CSS树结合生成Render Tree(渲染树)——这是由可视化元素按照其显示顺序组成的树形结构,是文档可视化的表示,它的作用是让浏览器能够按照正确的顺序渲染页面元素。Firefox中称之为“框架”,Webkit中的术语则是呈现器或者呈现对象。
渲染树是和DOM元素相对应的,但是并非全部一一对应,例如:1,非可视化元素是不会出现在渲染树中,如“head”元素,2,如果元素的display属性值为“none”,也不会出现在渲染树中(但是visibility属性值为“hidden”的元素会出现在渲染树中)
渲染树中并不包含位置和大小的信息,计算这些值的过程就是布局或者重排。
布局的过程是一个递归的过程,从根元素开始,递归遍历部分或者所有的渲染树结构,并为每一个需要显示元素计算几何信息。一般根元素位置坐标(0,0),大小为浏览器窗口的可见区域。
这里涉及到两个重要的概念reflow和repaint:
repaint(重绘):元素的某一部分属性发生改变,如字体颜色,背景颜色等改变,尺寸并未改变,这时发生的改变过程就是repaint。
reflow(回流): 因为浏览器渲染是一个由上而下的过程,当发现某部分的变化影响了布局时,就需要倒回去重新渲染,这个过程就称之为reflow。reflow几乎是没法避免的,现在一些常用的效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。基本上能引起reflow的主要有几个原因:
1、网页初始化。
2、JS操作DOM树的时候,增加删除元素等。
3、某些元素的尺寸改变。
4、CSS属性的改变,
但是浏览器很聪明,为了避免细小的改变就进行repaint或者reflow,浏览器采用一种"dirty"系统,会将这些改变操作积攒一批,然后做一次reflow,这又叫异步reflow或增量异步reflow。但是有些特殊情况不会这么做,比如:resize窗口,改变了页面默认的字体,等,对于这些操作,浏览器会马上进行reflow。
但是有的时候,我们自己编写的脚本会阻止浏览器的这种操作,比如我们请求下面的值的时候:offsetTop, offsetLeft, offsetWidth, offsetHeight,scrollTop/Left/Width/Height,clientTop/Left/Width/Height,IE中的 getComputedStyle(), 或 currentStyle等,如果我们的程序运行的时候需要这些值,那么浏览器需要给我们返回最新的值,而这样就会将当前积攒的操作执行,从而引起频繁的reflow或者repaint。
通常reflow比repaint会耗费更多的时间,从而也就会影响性能,所以编写代码的时候要尽可能避免过多的reflow或者repaint。减少reflow/repaint的方法:
1,修改样式不要逐条修改,建议定义CSS样式的class,然后直接修改元素的className。
2,不要将DOM节点的属性值放在循环中当成循环的变量。
3,为动画的 HTML 元素使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
4,把DOM离线后修改。如设置DOM的display:none,然后进行你需要的多次修改,然后再显示出来,或者clone一个节点到内存中,然后随意修改,修改完成后再与在线的交换。
5,千万不要使用table布局,一个微小的改变就可能引起整个table的重新布局。
渲染页面
在绘制阶段,系统会遍历渲染树,并且调用呈现器将的“paint”方法,将内容显示在屏幕上。同样,类似于布局过程,也分为全局和增量两种
- 页面精简,删除不必要的注释,空格,将内嵌的JS和CSS移至外部文件,使用压缩工具等。
- 减少文件数量,减少页面上引入的文件数量可以减少请求的次数,可以合并的JS和CSS文件尽量合并。
- 减少域名查询,DNS查询和解析域名需要消耗时间,减少对外部JavaScript、CSS、图片等资源的引用,不同域名的使用越少越好。
- 使用缓存,重用数据。
- 优化页面元素的加载顺序。
- 使用现在CSS和合法的标签。
- 指定图片的大小,如果浏览可以立即确定图片大小就不需要重新进行布局操作。
- 根据浏览器类型选择合适的策略。
- 使用压缩工具等。
- 页面精简,删除不必要的注释,空格,将内嵌的JS和CSS移至外部文件,使用压缩工具等。
首先说明CSS选择符的匹配顺序,从右到左!从右到左!从右到左!(重要的事情说三遍),所以,类似于“#nav li” 我们以为很简单的规则,应该马上就可以匹配成功,但是,需要从右往左匹配,所以,先会去查找所有的li,然后再去确定它的父元素是不是#nav。因此,编写合理的CSS也可以提高我们的页面行能:
- DOM的深度尽量浅,不要嵌套过深。
- 减少inline javascript css的数量。
- 使用合法的CSS属性。
- 不要为ID选择器指定类名或者标签名。
- 避免后代选择器,尽量使用子选择器。
- 避免使用通配符。
对于javascript标签首先得了解其加载和执行的特点:1,载入后立即执行,2,执行时会阻塞页面后续的内容,针对这些特点,我们使用javascript标签时应该注意:
- 将所有的javascript标签放在页面底部,也就是body标签闭合之前,这样可以保证脚本执行前已完成DOM渲染。
- 尽可能合并脚本,页面中引入的脚本越少,加载响应速度也就越快。
- 减少inline javascript的使用。
- 所有的javascript标签会按照其引入顺序依次执行,只有前面的内容解析完成才会解析下一个,所以注意多个javascript标签的引入顺序。
- 使用defer属性,该属性可以使脚本在文档完全呈现以后再执行。
- 使用async属性,可以使当前脚本不必等待其他脚本的执行,也不必阻塞文档的呈现。
如有不妥,欢迎指正!