先来一道经典的面试题:
在浏览器地址栏输入url,回车后发生了什么。
这个问题的答案,相信大家都背的比较熟(网上扒到的一份答案)
1、解析URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
2、缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
3、DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
4、获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
5、TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
6、HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
7、返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
8、页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
9、TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
接下来面试官可能会问,在这个过程中,有哪些环节可以做性能优化,巴拉巴拉的。下面我会列举一些性能优化的方案。分为2个部分,请求响应优化和渲染优化
请求响应优化
DNS解析
当浏览器从(第三方)服务器请求资源时,必须先将跨域域名解析为ip地址,然后浏览器才能发出请求,这个过程就叫DNS解析。
如果想查看DNS解析,一般情况下需要先将DNS缓存给清除掉
1.清除浏览器DNS缓存
-
清除DNS缓存:
chrome://net-internals/#/dns -
清除套接字缓存池:
chrome://net-internals/#/socket2.清除系统DNS缓存 -
查看DNS缓存:
ipconfig/displaydns -
清除DNS缓存:
ipconfig/flushdns清除之后,在网站的network下,资源文件的WaterFail里面就能看到DNS解析的时间了
DNS解析优化,一般有2种方式
-
减少DNS解析的请求次数
- 延长DNS缓存时间:大多数浏览器都有自己的DNS缓存策略,ie缓存30分钟,firefox和chrome缓存1分钟
- 尽量减少页面中所用的域名数量。需要注意的是,减少DNS查找会减少响应时间,但是减少并行下载可能增加响应时间。个人建议,将资源划分为2~4个域名。
-
进行DNS预获取:
DNS Prefetch:-
使用方式:
<link rel="dns-prefetch" href="//xxx.com" /> -
注意事项:
-
仅对跨域的DNS查找有效
-
多页面重复DNS与解析会增加DNS查询次数
-
-
-
使用CDN加速域名:就近原则获取资源
http
http1.1
-
长连接:以往请求一个html文件的时候,如果html文件里面引入了其他资源,比如图片等,每次请求都会进行tcp的建立和断开的过程。所以在
http1.1里面引入了持久链接,即tcp连接默认不关闭,可以被多个请求复用。 -
http1.1的管道机制:http1.1里面,允许在同一个tpc连接里面,发送多个请求。以往发送请求后需要等待并接受相应,然后才能进行下一个请求。管道机制出现后,可以不用等待直接发送下一个请求
长连接的缺点:虽然http1.1允许复用tcp连接,但是在一个tcp连接里面,所有的数据通信时按顺序进行的,只有处理完一个回应,才会进行下一个回应。如果前面的回应处理特别慢,就会导致整个连接回应变慢,这叫做“队头阻塞”。
如何避免
-
减少请求数
-
同时多开长连接
http2
- 二进制协议
-
http1.1:头信息是文本,数据体可以使文本也可以使二进制
-
http2:头信息和数据体都是二进制
-
相比于文本的解析数据,二进制解析要方便的多。
-
多工:http2复用tcp连接的时候,在一个连接里,允许客户端和浏览器同时发送多个请求和相应,并且不用按照顺序一一对应,能够避免“队头阻塞”。
-
数据流:因为http2不是按顺序响应的,所以同一个链接里面的数据包可能属于不同的响应,因此需要对数据包做标记。http2将每个请求或回应的所有数据包,称为1个数据流,每个数据流都有一个独一无二的编号
-
头信息压缩
-
http协议不带有状态,每次请求,都必须带上所有的信息,比如cookie,会造成带宽浪费
-
http2做了优化,引入头信息压缩机制。一方面,头信息可以使用gzip或compress压缩后在发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段会存入这张表,生成一个索引号,以后就不需要发送通用的字段了,只发送索引号,这样就能提高速度了。
-
-
服务器推送:http2允许服务器未经同意,主动向客户端发送资源。 场景:客户端请求一个网页,这个网页包含很多静态资源。一般情况下,客户端必须受到网页后,解析html代码,发现静态资源,在发送静态资源请求。其实,服务器可以预期到客户端可能会继续请求静态资源,所以就主动把这些静态资源随着网页一起发送了。
压缩传输的数据源
-
使用
gzip压缩响应文本资源 -
http2支持请求头信息压缩
http缓存
强制缓存
* 在响应头里面设置`expires: [UTC格式的时间戳]`,表示,在这个时间之前,用缓存的数据,超过这个时间,重新请求资源,这种方式过于依赖本地时间,如果本地时间跟服务器时间不一致,就会出问题。
* 在响应头里面设置`cache-control: max-age=123`,max-age的单位是秒,表示在这个响应返回之后,n秒之内采用缓存,过了n秒,重新请求数据
cache-control其他的配置
-
no-cache:注意,这不是不使用缓存的意思,而是表示强制使用协商缓存 -
no-store:不使用任何缓存策略 -
s-maxage:代理服务器的缓存有效时间
协商缓存
lastmodified
第一种方式,cache-control结合lastModified来实现
在响应头里面设置cache-control: no-cache以及last-modified: [UTC格式的时间戳]。在请求头里面对应有一个字段叫做if-modified-since。如果采用了协商缓存,在浏览器发送请求的时候,服务器会将if-modified-since和last-modified进行对比,如果一致,就会返回304状态码,说明当前缓存是有效的,直接从缓存拿资源就行了。
lastModified的缺点
-
只是根据资源最后修改时间来进行判断的,如果只是对资源进行了编辑,但内容没有发生变化的话,时间戳也会变化,这个时候其实我们是不希望重新请求资源的,但是
lastmodified识别不了这种情况,会重新发送请求 -
文件资源的修改时间戳单位是秒,如果修改在1秒内完成,
lastmodified也是识别不了的,会认为文件没有变化,直接走缓存,从而导致资源没有更新
Etag
- 响应头里面:
etag: [文件指纹,通常是一段字符串] - 请求头里面:
If-None-Match:[文件指纹,通常是一段字符串]原理是根据文件资源的内容生成特定的文件指纹,只要文件内容发生变化,文件指纹也会相应的变化。这样就可以解决lastmodified的缺陷。需要注意的是,etag不是lastmodified的替代方法,而是补充方案,也就是说要结合使用
Etag的缺点
-
服务器生成Etag需要额外的开销,如果文件资源过大,会导致性能问题
-
Etag分为强验证和弱验证。强验证根据文件内容进行生成,保证每个字节都相同。弱验证根据文件内容的部分属性来生成,生成速度快,但是无法保证验证的准确性
CDN缓存
从最近的服务器请求资源。适用于静态资源的缓存。
与前端关系密切的CDN优化点:域名设置。以淘宝为例,淘宝主域名为www.taobao.com,静态资源请求的CDN服务器域名有g.alicdn.com和img.alicdn.com,CDN服务器是有意设计成跟主域名不同的,主要有2点考虑
-
避免静态资源请求携带不必要的信息,如cookie
-
考虑浏览器对同一域名下并发请求的限制,如chrome同一域名最大并发数目限制为6个。
渲染优化
浏览器从获取html到最终呈现在页面上,需要以下几个步骤
- 处理html标记构建dom树
- 处理css标记构建cssom树
- 将dom树和cssom树合并成一个render tree
- 根据渲染树来布局,以计算每个节点的几何信息
- 将各个节点绘制到屏幕上
上面的步骤就涉及到关键渲染路径优化,意思就是说最大程度缩短上面5步的时间。
关键渲染路径优化
-
html文件尽可能小。通常html文件中会有一些js注释,css注释,空格,换行等。但这些东西其实只在开发环境才需要。对于生产环境来说,要尽可能保证html文件的精简。比如采用gzip压缩,http缓存
-
css会阻塞网页加载。
- 所以一些在当前页面不需要的css文件,我们需要将它设置为非阻塞的,在
link标签上增加media属性。例如<link href="other.css" ref="stylesheet" media="min-width: 40em" - 避免使用
@import引入css资源,@import引入的css会增加关键渲染路径的长度。@import引入的css资源时串行加载的,所以可能会导致加载时间变长
- 所以一些在当前页面不需要的css文件,我们需要将它设置为非阻塞的,在
-
js优化
- 异步加载js,使用
defer、async,加了这2个属性后,浏览器不会等待js解析。不同点是加了defer的脚本会按顺序加载,适用于后面的js对前面的js有依赖的情况,而加了async的谁先加载完,谁先执行<script defer src=""></script><script async src=""></script> - 避免同步请求
- 延迟解析js
- 避免运行时间长的js
- 异步加载js,使用
-
使用
requestAnimationFrame实现动画:通常我们在js里面实现动画的时候都会使用setInterval来实现,这种方法智能设定特定的刷新频率,在不同刷新率的屏幕上会出现卡顿效果。而使用requestAnimationFrame会将刷新频率交给系统来决定,这样就能消除setInterval造成的卡顿效果。需要注意的是这个属性ie10及以上才支持 -
使用web work
Web Worker 有以下几个使用注意点。
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
基本用法
主线程里面
var worker = new Work('worker.js')
worker.postMessage({data}) // 如果需要传参数给子线程调用这个方法
work.addEventlistener('message', e => {
// e.data里面可以拿到worker返回的数据
var { data } = e.data
worker.terminate() //work用完要手动关闭
})
子线程里面
this.addEventlistener('message', e => {
var data = e.data;
//处理完data之后,用postMessage传给主线程
this.postMessage({data})
this.close(); // 也可以在子线程里调用close关闭子线程
})
函数防抖和节流
节流:简单来说,就是某段时间内,无论触发了多少次回调,在计时结束之后只响应第一次的触发。适合在页面滚动的时候执行某些操作的场景。实现原理如下
function throttle(fn, delay) {
return function(arguments) {
let args = arguments;
// 间隔时间内重复调用的话,清除计时器,让fn不执行
clearTimeout(fn.id);
fn.id = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
防抖:会稀释回调的执行次数,频繁触发回调的时候,在某段时间内执行一次。适合频繁点击按钮,减少请求次数的场景。实现原理如下
function debounce (fn, delay) {
let lastTime
let timer
delay || (delay = 300) // 默认间隔300毫秒
return function(arguments) {
let args = arguments;
let nowTime = +new Date(); // + 运算符会将事件对象转为时间戳格式
if (lastTime && nowTime < lastTime + delay) {
// 当前距离上次执行的时间小于设置的时间间隔
clearTimeout(timer) // 清除定时器
timer = setTimeout(() => {
lastTime = nowTime
fn.apply(this, args)
}, delay)
} else {
// 当前距离上次执行的时间大于设置的时间间隔,直接执行函数
lastTime = nowTime;
fn.apply(this, args)
}
}
}