HTTP系列:HTTP过程及网络层的前端性能优化

732 阅读13分钟

客户端和服务器之间的信息通信有多重方式:

  • XMLHttpRequest/ajax/axios/$.ajax/fetch数据交互
  • 跨域处理方案:ajax、fetch、jsonp、postMessage...
  • 资源获取:(html/css/js/image/音视频...)
  • webscoket
  • ...

一次HTTP过程包括:

  • 客户端把信息传递给服务器或者向服务器发送请求(请求 Request)
  • 服务器端接收客户端信息并且返回给客户端相关的内容(响应 Response)
  • 请求+响应=一次HTTP事务

客户端和服务器端之间传输的所有内容,统称为HTTP报文。一次HTTP报文包括以下信息:

  • 起始行:基本信息(包含HTTP的版本等)。
    • 请求起始行 'GET{请求方式} /res-min/themes/marxico.css{请求地址} HTTP/1.1{HTTP版本号}'
    • 响应起始行 'HTTP/1.1{HTTP版本号} 200{HTTP响应状态码} OK{状态码描述}'
  • 首部(头):请求头(客户端->服务器)、响应头(服务器->客户端)
  • 主体:请求主体(客户端->服务器)、响应主体(服务器->客户端)

客户端和服务器之间的数据传输,依托于网络(通信模式 TCP/UDP... & 传输协议 HTTP/HTTPS/FTP...)。那么,这个过程详细是怎么样的呢?

从输入URL地址到看到页面,中间都经历了什么?如何优化这一过程?

老生常谈的问题

  1. URL解析(识别URL)
  2. 检查缓存(强缓存、协商缓存{针对于资源文件请求} & 本地存储{针对于数据请求})
  3. DNS服务器解析(域名解析:根据域名解析出服务器的外网IP)
  4. TCP三次握手(建立客户端和服务器之间的网络连接通道)
  5. 基于HTTP/HTTPS等传输协议,实现客户端和服务器之间的信息通信
  6. TCP四次挥手(把建立好的网络通道释放掉)
  7. 客户端渲染(呈现出页面和效果)

下面详细说说每个过程

URL解析(识别URL)

image。png

URI统一资源标识符,包括

  • URL:统一资源定位符
  • URN:统一资源名称

一段URL 'http://user:pass@www.baidu.cn:80/st/index.html?xxx=xxx&xxx=xxx#video' ,结构分析如下:

传输协议

传输协议:http / https / ftp ...

  • HTTP超文本传输协议。即除了传输文本(例如字符串等)还可以传输其余的信息(例如:文件流、二进制或者Buffer格式再或者BASE64格式的数据)
  • HTTPS=HTTP+SSL 更加安全的HTTP。传输的内容经过加密处理,一般涉及支付类的产品都采用这种协议
  • FTP文件传输协议,一般用于直接基于一些FTP工具(例如filezilla),把开发的文件部署到服务器上
  • ...

登录认证信息

用户名密码:user:pass,一般是不用的

域名

域名:www.baidu.cn

  • 顶级域名 baidu.cn
  • 一级域名 www.baidu.cn
  • 二级域名 video.baidu.cn
  • 三级域名 student.video.baidu.cn
  • ...

购买的是顶级域名,自己后期可以分配二级/三级域名。

域名的目的就是给对应的服务器外网IP起一个别名,方便用户记忆

域名和服务器都购买完成后,需要在DNS服务器上生成一条解析记录,用于以后的DNS解析

.com / .cn / .net / .org / .gov /不同的后缀也有一些不同的意义

协议、域名、端口号只要有一个不同,则为跨域。跨域的问题后面写文章单独讲。以下皆为跨域:

  • 'www.baidu.com' VS 'www.qq.com' :跨域
  • 'www.baidu.com' VS 'video.baidu.com' :跨域(主域相同,但是子域不同)
  • ' www.baidu.com:80' VS 'www.baidu.com:443' :跨域
  • 'http://www.baidu.com' VS 'https://www.baidu.com' :跨域

下面为同源:

'http://www.baidu.com:80/st.html' VS 'http://www.baidu.com:80/index.html' :同源

端口号

端口号:80。端口就是用来区分一台服务器上的多个项目的(每一个项目其实都是一个服务)。 取值范围 0~65535之间

默认端口号:在浏览器地址栏中输入地址,我们不写端口号,浏览器会帮助我们加上,传递给服务器的时候是带着端口号的。http->80 , https->443,ftp->21。

请求资源的路径名称

请求资源的路径名称:/st/index.html,可以基于路径找到客户端需要的资源文件。

用户看到的URL地址可能是重写后的(看到的地址在文件目录中不存在),例如ajax数据请求的接口地址为/api/list,后台可以根据这个不存在的地址返回其他相应的东西

问号参数信息(查询字符串)

问号参数信息:?xxx=xxx&xxx=xxx

  • 把信息传递给服务器。GET系列请求一般都是这样传递参数 。 xxx=xxx&xxx=xxx这种格式叫做x-www-form-urlencoded格式
  • 如果是页面跳转,查询字符串可以把信息传递给另外一个页面

片段标识符(HASH值)

HASH(哈希)值:#video 一般用作:

  • 锚点定位
  • HASH路由

其他问题(URL编译问题)

如果一段url如下

let url = ` http://www.xxx.com/index.html?lx=1&from=http://www.qq.com/'.

其中查询字符串from包含一个完整的域名,浏览其解析的时候就会出问题, http://www.xxx.com/index.html?lx=1&from= 会被解析为一个url, http://www.qq.com/ 会被解析为另一个url。

所以我们要对整个url或者后面的查询字符串进行编码,让浏览器只识别成为一个url

编码分类:

  • encodeURI & decodeURI :编译空格和中文,一般编译整个URL中的信息
    let url =  ` http://www.xxx.com/s t/index.html?x=1&name=你好&from=http://www.qq.com ` 
    console.log(encodeURI(url))
    

image。png

空格和中文被编码

  • encodeURIComponent & decodeURIComponent :编译空格和中文以及一些特殊符号,所以一般只是用来编译传递的信息值的,而不是整个URL,以解决URL解析不了或者传递信息的乱码等问题。
        let url =  ` http://www.xxx.com/st/index.html?x=1&name=${encodeURIComponent('你 好')}&from=${encodeURIComponent('http://www.qq.com')} ` 
        console.log(url)
    

image。png

  • escape & unescape :用于客户端页面信息传递或者一些信息的编译的「例如:cookie中的中文内容编译」
    
    console.log(escape('你好'));
    console.log(encodeURIComponent('你好'));
    

image.png

检查缓存

缓存处理是基于HTTP网络层进行优化的一个非常重要的手段「针对的资源文件请求」

检查缓存的两种方式

  • 强缓存、协商缓存(针对于资源文件请求)
  • 本地存储(针对于数据请求)

缓存位置

  • Memory Cache : 内存缓存
  • Disk Cache:硬盘缓存

区别:

  • 打开网页时:浏览器会首先查找 Disk Cache中是否有匹配的缓存,如有则使用,如没有则发送网络请求。
  • 普通刷新 (F5)时:因TAB没关闭,因此Memory Cache是可用的,所以刷新时,如果内存中有缓存,会被优先使用,其次才去查找Disk Cache
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache,服务器直接返回 200 和最新内容

image.png

image.png

强缓存

强缓存:Expires / Cache-Control

image.png

强缓存的作用过程是这样的:

  • 第一次请求资源时返回请求结果和缓存标识(响应头Expires/ Cache-Control),把请求结果和缓存表示存储在浏览器缓存当中
  • 缓存标识的作用机理:
    • Expires:缓存过期时间,用来指定资源到期的时间(HTTP/1.0)
    • Cache-Control:会返回如下样子的响应头 'Cache-Control: max-age=2592000' ,意思为第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
  • 如果再次发送请求,会先检测缓存信息和缓存标识Expires/ Cache-Control,如果有,且未过期,那么客户端直接读取缓存的信息,不再发送请求给服务器
  • 注意如果两者同时存在的话,Cache-Control优先级高于Expires

问题:本地缓存了文件,但是服务对应的资源文件更新了,我们如何保证获取的是最新的内容?

  • 请求资源文件的时候设置时间戳

    例如:第一次 <link href='index.css?20210224215800'> ,第二次 <link href='index.css?20210227000000'> 。如果服务器资源有更新,再次发请求,保证时间戳不一样,这样就不会走本地的强缓存了,而是从新拉取最新的资源

  • 文件HASH名

    例如:第一次 <link href='dasdasd43546.css'> ,如果服务器资源更新,文件名字重新HASH(webpack) <link href='75675675fsdff6.css'> ,这样就不会走本地的强缓存了

所以HTML文件永远不会去做强缓存

第一次

<html>
    <link href='dasdasd43546.css'>
</html>

第二次

<html>
   <link href='75675675fsdff6.css'>
</html>

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

image.png

协商缓存的作用过程是这样的(以Etag举例):

  • 首先第一次发送请求,获得的响应包含响应头(假设为Etag:s35b56f)和响应主体,然后把内容缓存下来
  • 第二次发送请求给后台,会携带缓存标识(If-Modfined-Since/If-None-Match)发送HTTP请求,例如请求头:If-Modfined-Since:s35b56f,这个标识就是第一次返回的Etag的一个标识
  • 服务器根据Etag判断文件是否更新
    • 没更新:返回304,不返回内容,通知客户端读取本地的缓存信息
    • 更新了:返回200,以及最新的资源信息,以及Last-Modfined/Etag的新的值
  • 浏览器接收到返回的信息
    • 如果是200:说明是最新的信息那就直接渲染,并且把最新的结果和表示缓存到本地
    • 如果是304:就从本地缓存中获取内容进行渲染

Last-Modified / ETag的意义:

  • Last-Modified:记录服务器资源文件最后一次更新的时间
  • ETag:只要服务器资源文件改变,会生成一个不同的标识

协商缓存的意义:

  • 当强缓存失效(或者不存在)(例如:html)那就可以做协商缓存,然后校验协商缓存即可
  • 每一次都会向服务器校验资源是否更新

image.png

可以看一下百度某个css文件的缓存设置 image.png

注意:

强缓存还是协商缓存都是服务器端设置的,客户端浏览器自己会根据返回的一些信息,进行相关处理,无需前端单独设置

注意:强缓存没有发送请求,在发送请求之前发现有缓存就用了本地的缓存内容,而协商缓存发送了请求(因为要询问后台是否使用缓存,与后台协商),如果返回了304,就用本地的缓存。

本地存储缓存

从这个角度来缓存,就需要使用js进行编码,逻辑处理

image.png

本地存储缓存的两种方式:

  • 页面不关闭,针对于不经常更新的数据,我们读取缓存数据,这种数据一般存在内存中,页面刷新,存储的数据就没有了
  • 页面关闭,重新打开,我们也可以读取缓存中的数据,这种数据就是持久化存储,我们可以自己设置过期时间

客户端存储数据的几种方案:

  • (全局)变量存储「vuex/redux」:页面刷新或者关闭后重新打开,之前存储的数据都没有了(内存释放问题导致的)
  • cookie
  • webStorage:localStorage & sessionStorage
  • IndexedDB
  • Cache
  • Manifest 离线存储

localStorage VS sessionStorage

HTML5新增的API「不兼容IE8及以下浏览器」

  • localStorage:持久化本地存储「没有过期时间」,页面关闭存储的内容也是在的,只有手动清除(或者卸载浏览器)才会清除
  • sessionStorage会话存储,页面关闭后,存储的信息会消失「但是页面刷新是不消失的」

localStorage VS cookie

地存储的数据是有同源访问限制的,只允许读取本源下存储的内容

  • cookie只允许一个源下最多存储4KB内容,所以不能存储太多的数据。localStorage可以同源下存储5MB内容!

  • cookie是需要设置过期时间的,超过时间就失效了,并且有路径等限制。localStorage是持久化存储,没有过期时间,除非自己设定一些过期的处理机制。

  • cookie不稳定「一些浏览器自带的清除操作,有可能会把cookie清除掉;开启无痕浏览或者隐私模式,则不能存储cookie信息。但是localStorage不受这些操作的影响。

    image.png

  • cookie兼容低版本浏览器

  • cookie不算严格的本地存储,和服务器之间有很多的联系。客户端向服务器发送请求的时候,会默认把本地的cookie信息,基于请求头发送给服务器;并且如果服务器返回的响应头中有Set-Cookie字段,浏览器也会默认把这些信息在客户端本地存在cookie中。localStorage是严格本地存储,默认情况下和服务器没有关系。

image.png

用代码来演示本地缓存的使用原理

将缓存数据存在全局变量中(刷新缓存消失):

let submit = document.querySelector('#submit'),
     runing = false;

let serverData;
submit.onclick = function () {
   if (runing) return;//简单的防抖处理
   runing = true;

   if (serverData) {
      // 如果有数据,直接使用缓存数据,无需从服务器获取
      console.log('请求回来的数据是:', serverData);
      runing = false;
      return;
   }

   // 从服务器拉去数据,并其存储到全局变量中
   axios.get('http://127.0.0.1:8888/home_banner').then(response => {
      console.log('请求回来的数据是:', response.data);
      serverData = response.data;
      runing = false;
   });
};

sessionStorage:

 submit.onclick = function () {
   if (runing) return;
   runing = true;

   let data = sessionStorage.getItem('@A');
   if (data) {
      // 如果有数据,直接使用缓存数据,无需从服务器获取
      console.log('请求回来的数据是:', JSON.parse(data));
      runing = false;
      return;
   }

   // 从服务器拉去数据
   axios.get('http://127.0.0.1:8888/home_banner').then(response => {
      console.log('请求回来的数据是:', response.data);
      sessionStorage.setItem('@A', JSON.stringify(response.data));
      runing = false;
   });

localStorage持久化存储要加上过期时间。

submit.onclick = function () {
   if (runing) return;
   runing = true;

   let data = localStorage.getItem('@A');
   if (data) {
      data = JSON.parse(data);
      // 自己可以设定过期的标准 1小时
      if (new Date() - data.time <= 60 * 60 * 1000) {
         console.log('请求回来的数据是:', data.data);
         runing = false;
         return;
      }
   }

   // 从服务器拉去数据,并其存储到全局变量中
   axios.get('http://127.0.0.1:8888/home_banner').then(response => {
      console.log('请求回来的数据是:', response.data);
      localStorage.setItem('@A', JSON.stringify({
         time: +new Date(),
         data: response.data
      }));
      runing = false;
   });
}

DNS服务器解析

两种解析方法

  • 递归查询

    image。png

  • 迭代查询 image。png

如果访问资源的域名的域名比较多,说明资源是部署到多台服务器上的,此时需要更多的DNS解析,导致消耗的时间会更多 image。png

多台服务器也有自己的好处:

  • 资源的合理利用,性能高的服务器就用来存储数据,性能不好的就用来存储静态资源,减少资金

  • 抗压能力加强,多台服务器减少压力

  • 提高HTTP并发,多台服务器可以提高总共服务器的并发请求 例如百度: image。png

    将不用的资源放到不同的服务器上

DN解析优化:

每一次DNS解析时间预计在20~120毫秒,可以使用DNS预获取(DNS Prefetch)来减少DNS请求次数。

例如:

<meta http-equiv="x-dns-prefetch-control" content="on">

<link rel="dns-prefetch" href="//static。360buyimg。com"/>

<link rel="dns-prefetch" href="//misc。360buyimg。com"/>

<link rel="dns-prefetch" href="//img10.360buyimg。com"/>

<link rel="dns-prefetch" href="//d。3.cn"/>

<link rel="dns-prefetch" href="//d。jd。com"/>

原理:link会单独开辟一个线程去加载资源,同时html继续向下渲染。所以可以单独开辟一个县城预先解析所有DNS并且缓存,如果下面再遇到这个域名,例如img的src中包含这个域名,那么DNS解析就不用重新再次解析了。

TCP三次握手

TCP三次握手的目的:让客户端和服务器端建立稳定可靠的传输通道,确定双方都可以收发信息。

UDP不需要三次握手,但是快且不稳定。

数据传输

HTTP请求与响应。

这部分可以看一下我以前写的这篇文章

HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin.cn)

TCP四次挥手

释放建立的链接通道

  • 服务器端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文
  • 但关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接(正在传输数据),所以只能先回复一个ACK报文,告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送
  • 等所有数据正在传输的数据全部传输完成,服务器才给再次发送一个FIN报文

故需要四步握手

TCP连接优化方法:

保持长链接,不关闭http通道,设置Connection: keep-alive请求头 image。png

客户端渲染

客户端渲染优化可以看我以前的两篇文章

浏览器渲染过程和CRP优化一:渲染过程 (juejin.cn)

浏览器渲染过程和CRP优化二:CRP优化 (juejin.cn)

网络层性能优化汇总

网络优化是前端性能优化的中的重点内容,因为大部分的消耗都发生在网络层,尤其是第一次页面加载,如何减少等待时间很重要。http层面的性能优化可以减少白屏的效果和时间

刚说了http的一次流程,中间顺便穿插一些优化的方法,这里做一个汇总,顺便补充一些内容

  1. 利用缓存

    • 对于静态资源文件实现强缓存和协商缓存(扩展:文件有更新,如何保证及时刷新)

    • 对于不经常更新的接口数据采用本地存储做数据缓存(扩展:cookie / localStorage / vuex|redux 区别)

  2. DNS优化

    • 分服务器部署,增加HTTP并发性(导致DNS解析变慢)

    • DNS Prefetch

  3. TCP的三次握手和四次挥手

    • Connection:keep-alive
  4. 数据传输

    • 减少数据传输的大小

      • 内容或者数据压缩(webpack等)

      • 服务器端一定要开启GZIP压缩(一般能压缩60%左右)

      • 大批量数据分批次请求(例如:下拉刷新或者分页,保证首次加载请求数据少)

    • 减少HTTP请求的次数

      • 资源文件合并处理

      • 字体图标,一些就不要发请求了,尽量使用字体去做,阿里图标

      • 雪碧图 CSS-Sprit

      • 图片的BASE64

  5. CDN服务器“地域分布式”

    在多个地方部署服务器,让各个地域的访问速度保持一致

  6. 采用HTTP2.0

减少白屏:

  • loading人性化体验

  • 骨架屏:客户端骨屏(其实也是一个loading) + 服务器骨架屏

  • 图片延迟加载

减少白屏也是一个值得探索的问题,以后写文章再详细说

HTTP几个版本的区别

HTTP1.0和HTTP1.1的一些区别

  • 缓存处理,HTTP1.0中主要使用 Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control
  • 带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(Partial Content)
  • 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除…
  • Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
  • 长连接,HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点

HTTP2.0和HTTP1.X相比的新特性

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮

  • header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小

  • 服务端推送(server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了

    // 通过在应用生成HTTP响应头信息中设置Link命令
    
    Link: </styles.css>; rel=preload; as=style, </example.png>; rel=preload; as=image
    
  • 多路复用(MultiPlexing)

    并发请求指的是建立多个链接通道,而不是在一个通道上进行多个HTTP请求

    • HTTP/1.0 每次请求响应,建立一个TCP连接,用完关闭

    • HTTP/1.1 「长连接」 ,但是,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;

    • HTTP/2.0 「多路复用」多个请求可同时在一个连接上并行执行,某个请求任务耗时严重,不会影响到其它连接的正常执行;