当面试官问出这个题后,大部分人听到都是心田窃喜:早就背下这篇八股文。
然而稍等,上面几个问题你能答出来吗:
- 浏览器对URL为什么要解析?URL参数用的是什么字符编码?那encodeURI和encodeURIComponent有什么区别?
- 浏览器缓存的disk cache和memory cache是什么?
- 预加载prefetch、preload有什么差异?
- JS脚本的async和defer有什么区别?
- TCP握手为什么要三次,挥手为什么要四次?
- HTTPS的握手有理解过吗?
同样的问题,能够拿来招聘P5也能够是P7,只是深度不同。所以我重新整理了一遍整个流程,本文较长,倡议先珍藏。
概述
在进入正题之前,先简略理解一下浏览器的架构作为前置常识。浏览器是多过程的工作的,“从URL输出到渲染”会次要波及到的,是浏览器过程、网络过程和渲染过程这三个:
- 浏览器过程负责解决、响应用户交互,比方点击、滚动;
- 网络过程负责解决数据的申请,提供下载性能;
- 渲染过程负责将获取到的HTML、CSS、JS解决成能够看见、能够交互的页面;
“从URL输出到页面渲染”整个过程能够分成网络申请和浏览器渲染两个局部,别离由网络过程和渲染过程去解决。
网络申请
网络申请局部进行了这几项工作:
- URL的解析
- 查看资源缓存
- DNS解析
- 建设TCP连贯
- TLS协商密钥
- 发送申请&接管响应
- 敞开TCP连贯
接下来会一一开展。
URL解析
浏览器首先会判断输出的内容是一个URL还是搜寻关键字。
如果是URL,会把不残缺的URL合成残缺的URL。一个残缺的URL应该是:协定+主机+端口+门路[+参数][+锚点]
。比方咱们在地址栏输出www.baidu.com
,浏览器最终会将其拼接成https://www.baidu.com/
,默认应用443端口。
如果是搜寻关键字,会将其拼接到默认搜索引擎的参数局部去搜寻。这个流程须要对输出的不平安字符编码进行本义(平安字符指的是数字、英文和多数符号)。因为URL的参数是不能有中文的,也不能有一些特殊字符,比方= ? &
,否则当我搜寻1+1=2
,如果不加以本义,url会是/search?q=1+1=2&source=chrome
,和URL自身的分隔符=
产生了歧义。
URL对非平安字符本义时,应用的编码叫百分号编码,因为它应用百分号加上两位的16进制数示意。这两位16进制数来自UTF-8编码,将每一个中文转换成3个字节,比方我在google地址栏输出“中文”,url会变成/search?q=%E4%B8%AD%E6%96%87
,一共6个字节。
咱们在写代码时常常会用的encodeURI
和 encodeURIComponent
正是起这个作用的,它们的规定根本一样,只是= ? & ; /
这类URI组成符号,这些在encodeURI
中不会被编码,但在encodeURIComponent
中通通会。因为encodeURI
是编码整个URL,而encodeURIComponent
编码的是参数局部,须要更加严格把关。
查看缓存
查看缓存肯定是在发动真正的申请之前进行的,只有这样缓存的机制才会失效。如果发现有对应的缓存资源,则去查看缓存的有效期。
- 在有效期内的缓存资源间接应用,称之为强缓存,从chrome网络面板看到这类申请间接返回200,size是
memory cache
或者disk cache
。memory cache
是指从资源从内存中被取出,disk cache
是指从磁盘中被取出;从内存中读取比从磁盘中快很多,但资源能不能调配到内存要取决于当下的零碎状态。通常来说,刷新页面会应用内存缓存,敞开后从新关上会应用磁盘缓存。 - 超过有效期的,则携带缓存的资源标识向服务端发动申请,校验是否能持续应用,如果服务端通知咱们,能够持续应用本地存储,则返回304,并且不携带数据;如果服务端通知咱们须要用更新的资源,则返回200,并且携带更新后的资源和资源标识缓存到本地,不便下一次应用。
DNS解析
如果没有胜利应用本地缓存,则须要发动网络申请了。首先要做的是DNS解析。
会顺次搜寻:
- 浏览器的DNS缓存;
- 操作系统的DNS缓存;
- 路由器的DNS缓存;
- 向服务商的DNS服务器查问;
- 向寰球13台根域名服务器查问;
为了节省时间,能够在HTML头部去做DNS的预解析:
<link rel="dns-prefetch" href="http://www.baidu.com" />
为了保障响应的及时,DNS解析应用的是UDP协定
建设TCP连贯
咱们发送的申请是基于TCP协定的,所以要先进行连贯建设。建设连贯的通信是打电话,单方都在线;无连贯的通信是发短信,发送方不论接管方,本人说本人的。
这个确认接管方在线的过程就是通过TCP的三次握手实现的。
- 客户端发送建设连贯申请;
- 服务端发送建设连贯确认,此时服务端为该TCP连贯分配资源;
- 客户端发送建设连贯确认的确认,此时客户端为该TCP连贯分配资源;
为什么要三次握手才算建设连贯实现?
能够先假如建设连贯只有两次会产生什么。把下面的状态图稍加批改,看起来一切正常。
但如果这时服务端收到一个生效的建设连贯申请,咱们会发现服务端的资源被节约了——此时客户端并没有想给它传送数据,但它却筹备好了内存等资源始终期待着。
所以说,三次握手是为了保障客户端存活,避免服务端在收到生效的超时申请造成资源节约。
协商加密密钥——TLS握手
为了保障通信的平安,咱们应用的是HTTPS协定,其中的S指的就是TLS。TLS应用的是一种非对称+对称的形式进行加密。
对称加密就是两边领有雷同的秘钥,两边都晓得如何将密文加密解密。这种加密形式速度很快,然而问题在于如何让单方晓得秘钥。因为
传输数据都是走的网络,如果将秘钥通过网络的形式传递的话,秘钥被截获,就失去了加密的意义。
非对称加密,每个人都有一把公钥和私钥,公钥所有人都能够晓得,私钥只有本人晓得,将数据用公钥加密,解密必须应用私钥。这种加密形式就能够完满解决对称加密存在的问题,毛病是速度很慢。
咱们采取非对称加密的形式协商出一个对称密钥,这个密钥只有发送方和接管方晓得的密钥,流程如下:
- 客户端发送一个随机值以及须要的协定和加密形式;
- 服务端收到客户端的随机值,发送本人的数字证书,附加上本人产生一个随机值,并依据客户端需要的协定和加密形式应用对应的形式;
- 客户端收到服务端的证书并验证是否无效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端;
- 服务端收到加密过的随机值并应用私钥解密取得第三个随机值,这时候两端都领有了三个随机值,能够通过这三个随机值依照之前约定的加密形式生成密钥,接下来的通信就能够通过该对称密钥来加密解密;
通过以上步骤可知,在TLS握手阶段,两端应用非对称加密的形式来通信,然而因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端应用对称加密的形式。
发送申请&接管响应
HTTP的默认端口是80,HTTPS的默认端口是443。
申请的根本组成是申请行+申请头+申请体
POST /hello HTTP/1.1User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3Host: www.example.comAccept-Language: en, miname=niannian
响应的根本组成是响应行+响应头+响应体
HTTP/1.1 200 OKContent-Type:application/jsonServer:apache{password:'123'}
敞开TCP连贯
等数据传输结束,就要敞开TCP连贯了。敞开连贯的被动方能够是客户端,也能够是服务端,这里以客户端为例,整个过程有四次握手:
- 客户端申请开释连贯,仅示意客户端不再发送数据了;
- 服务端确认连贯开释,但这时可能还有数据须要解决和发送;
- 服务端申请开释连贯,服务端这时不再须要发送数据时;
- 客户端确认连贯开释;
为什么要有四次挥手
TCP 是能够双向传输数据的,每个方向都须要一个申请和一个确认。因为在第二次握手完结后,服务端还有数据传输,所以没有方法把第二次确认和第三次合并。
被动方为什么会期待2MSL
客户端在发送完第四次的确认报文段后会期待2MSL才正真敞开连贯,MSL是指数据包在网络中最大的生存工夫。目标是确保服务端收到了这个确认报文段,
假如服务端没有收到第四次握手的报文,试想一下会产生什么?在客户端发送第四次握手的数据包后,服务端首先会期待,在1个MSL后,它发现超过了网络中数据包的最大生存工夫,然而本人还没有收到数据包,于是服务端认为这个数据包曾经失落了,它决定把第三次握手的数据包从新给客户端发送一次,这个数据包最多破费一个MSL会达到客户端。
一来一去,一共是2MSL,所以客户端在发送完第四次握手数据包后,期待2MSL是一种兜底机制,如果在2MSL内没有收到其余报文段,客户端则认为服务端曾经胜利承受到第四次挥手,连贯正式敞开。
浏览器渲染
下面讲完了网络申请局部,当初浏览器拿到了数据,剩下须要渲染过程工作了。浏览器渲染次要实现了一下几个工作:
- 构建DOM树;
- 款式计算;
- 布局定位;
- 图层分层;
- 图层绘制;
- 显示;
构建DOM树
HTML文件的构造没法被浏览器了解,所以先要把HTML中的标签变成一个能够给JS应用的构造。
在控制台能够尝试打印document,这就是解析进去的DOM树。
款式计算
CSS文件一样没法被浏览器间接了解,所以首先把CSS解析成样式表。
这三类款式都会被解析:
- 通过 link 援用的内部 CSS 文件
<style>
标签内的款式- 元素的 style 属性内嵌的 CSS
在控制台打印document.styleSheets
,这就是解析出的样式表。
利用这份样式表,咱们能够计算出DOM树中每个节点的款式。之所以叫计算,是因为每个元素要继承其父元素的属性。
<style> span { color: red } div { font-size: 30px }</style><div> <span>年年</span></div>
比方下面的年年
,不仅要承受span设定的款式,还要继承div设置的。
DOM树中的节点有了款式,当初被叫做渲染树。
为什么要把CSS放在头部,js放在body的尾部
在解析HTML的过程中,遇到须要加载的资源特点如下:
- CSS资源异步下载,下载和解析都不会阻塞构建dom树
<link href='./style.css' rel='stylesheet'/>
- JS资源同步下载,下载和执行都会阻塞构建dom树
<script src='./index.js'/>
因为这样的个性,往往举荐将CSS样式表放在head头部,js文件放在body尾部,使得渲染能尽早开始。
CSS会阻塞HTML解析吗
上文提到页面渲染是渲染过程的工作,这个渲染过程中又细分为GUI渲染线程和JS线程。
解析HTML生成DOM树,解析CSS生成样式表以及前面去生成布局树、图层树都是由GUI渲染线程去实现的,这个线程能够一边解析HTML,一边解析CSS,这两个是不会抵触的,所以也提倡把CSS在头部引入。
然而在JS线程执行时,GUI渲染线程没有方法去解析HTML,这是因为JS能够操作DOM,如果两者同时进行可能引起抵触。如果这时JS去批改了款式,那此时CSS的解析和JS的执行也没法同时进行了,会先等CSS解析实现,再去执行JS,最初再去解析HTML。
从这个角度来看,CSS有可能阻塞HTML的解析。
预加载扫描器是什么
下面提到的外链资源,不论是同步加载JS还是异步加载CSS、图片等,都要到HTML解析到这个标签能力开始,这仿佛不是一种很好的形式。实际上,从2008年开始,浏览器开始逐渐实现了预加载扫描器:在拿到HTML文档的时候,先扫描整个文档,把CSS、JS、图片和web字体等提前下载。
js脚本引入时async和defer有什么差异
预加载扫描器解决了JS同步加载阻塞HTML解析的问题,然而咱们还没有解决JS执行阻塞HTML解析的问题。所有有了async和defer属性。
- 没有 defer 或 async,浏览器会立刻加载并执行指定的脚本
- async 属性示意异步执行引入的 JavaScript,经加载好,就会开始执行
- defer 属性示意提早到DOM解析实现,再执行引入的 JS
在加载多个JS脚本的时候,async是无程序的执行,而defer是有程序的执行
preload、prefetch有什么区别
之前提到过预加载扫描器,它能提前加载页面须要的资源,但这一性能只对特定写法的外链失效,并且咱们没有方法依照本人的想法给重要的资源一个更高的优先级,所以有了preload和prefetch。
- preload:以高优先级为以后页面加载资源;
- prefetch:以低优先级为前面的页面加载将来须要的资源,只会在闲暇时才去加载;
无论是preload还是prefetch,都只会加载,不会执行,如果预加载的资源被服务器设置了能够缓存cache-control
那么会进入磁盘,反之只会被保留在内存中。
具体应用如下:
<head> <!-- 文件加载 --> <link rel="preload" href="main.js" as="script"> <link rel="prefetch" href="news.js" as="script"></head><body> <h1>hello world!</h1> <!-- 文件文件执行 --> <script src="main.js" defer></script></body>
为了保障资源正确被预加载,应用时须要留神:
- preload的资源应该在以后页面立刻应用,如果不加上script标签执行预加载的资源,控制台中会显示正告,提醒预加载的资源在以后页面没有被援用;
- prefetch的目标是取将来会应用的资源,所以当用户从A页面跳转到B页面时,进行中的preload的资源会被中断,而prefetch不会;
- 应用preload时,应配合as属性,示意该资源的优先级,应用
as="style"
属性将取得最高的优先级,as ="script"
将取得低优先级或中优先级,其余能够取的值有font/image/audio/video
; preload字体时要加上
crossorigin
属性,即便没有跨域,否则会反复加载:<link rel="preload href="font.woff" as="font" crossorigin>
此外,这两种预加载资源不仅能够通过HTML标签设置,还能够通过js设置
var res = document.createElement("link"); res.rel = "preload"; res.as = "style"; res.href = "css/mystyles.css"; document.head.appendChild(res);
以及 HTTP 响应头:
Link: </uploads/images/pic.png>; rel=prefetch
布局定位
下面具体的讲述了HTML和CSS加载、解析过程,当初咱们的渲染树中的节点有了款式,然而不晓得要画在哪个地位。所以还须要另外一颗布局树确定元素的几何定位。
布局树只取渲染树中的可见元素,意味着head标签,display:none
的元素不会被增加。
图层分层
当初咱们有了布局树,但仍旧不能间接开始绘制,在此之前须要分层,生成一棵对应的图层树。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
因为页面中有很多简单的成果,如一些简单的 3D 变换、页面滚动,或者应用 z-index 做 z 轴排序等,咱们心愿能更加不便地实现这些成果。
并不是布局树的每个节点都能生成一个图层,如果一个节点没有本人的层,那么这个节点就从属于父节点的图层
通常满足上面两点中任意一点的元素就能够被晋升为独自的一个图层。
1、领有层叠上下文属性的元素会被晋升为独自的一层:明确定位属性position
的元素、定义通明属性opacity
的元素、应用 CSS 滤镜filter
的元素等,都领有层叠上下文属性。
2、须要剪裁(clip)的中央也会被创立为图层overflow
在chrome的开发者工具:更多选项-更多工具-Layers
能够看到图层的分层状况。
图层绘制
在实现图层树的构建之后,接下来终于到对每个图层进行绘制。
首先会把图层拆解成一个一个的绘制指令,排布成一个绘制列表,在上文提到的开发者工具的Layers面板中,点击detail中的profiler能够看到绘制列表。
至此,渲染过程中的主线程——GUI渲染线程曾经实现了它所有工作,接下来交给渲染过程中的合成现成。
合成线程接下来会把视口拆分成图快,把图块转换成位图。
至此,渲染过程的工作全副实现,接下来会把生成的位图还给浏览器过程,最初在页面上显示。
性能优化,还能够做些什么
本篇不专讲性能优化,只是在这个命题下补充一些常见伎俩。
预解析、预渲染
除了上文提到的应用preload、prefetch去提前加载,还能够应用DNS Prefetch
、Prerender
、Preconnect
DNS Prefetch:DNS 预解析;
<link rel="dns-prefetch" href="//fonts.googleapis.com">
preconnect:在一个 HTTP 申请正式发给服务器前事后执行一些操作,这包含 DNS 解析,TLS 协商,TCP 握手;
<link href="https://cdn.domain.com" rel="preconnect" crossorigin>
Prerender:获取下个页面所有的资源,在闲暇时渲染整个页面;
<link rel="prerender" href="https://www.keycdn.com">
缩小回流和重绘
回流是指浏览器须要从新计算款式、布局定位、分层和绘制,回流又被叫重排;
触发回流的操作:
- 增加或删除可见的DOM元素
- 元素的地位发生变化
- 元素的尺寸发生变化
- 浏览器的窗口尺寸变动
重绘是只从新像素绘制,当元素款式的扭转不影响布局时触发。
回流=计算款式+布局+分层+绘制;重绘=绘制。故回流对性能的影响更大
所以应该尽量避免回流和重绘。比方利用GPU减速来实现款式批改,transform/opacity/filters
这些属性的批改都不是在主线程实现的,不会重绘,更不会回流。
结语
把“URL输出到渲染”整个过程讲完,回到结尾几个比拟刁钻的问题,在文中都不难找到答案:
- 浏览器将输出内容解析后,拼接成残缺的URL,其中的参数应用的是UTF-8编码,也就是咱们开发时会罕用的encodeURI和encodeURIComponent两个函数,其中encodeURI是对残缺URL编码,encodeURIComponent是对URL参数局部编码,要求会更严格;
- 浏览器缓存的disk cache和memory cache别离是从磁盘读取和从内存中读取,通常刷新页面会间接从内存读,而敞开tab后从新关上是从磁盘读;
- 预加载prefetch是在闲暇工夫,以低优先级加载后续页面用到的资源;而preload是以高优先级提前加载以后页面须要的资源;
- 脚本的async是指异步加载,实现加载立即执行,defer是异步加载,实现HTML解析后再执行;
- TCP握手须要三次的三次是为了保障客户端的存活,避免服务端资源的节约,挥手要四次是因为TCP是双工通信,每一个方向的连贯开释、应答各须要一次;
- HTTPS的握手是为了协商出一个对称密钥,单方一共发送三个随机数,利用这三个随机数计算出只有单方晓得的密钥,正式通信的内容都是用这个密钥进行加密的;
如果这篇文章对你有帮忙,帮我点个赞呗~这对我很重要