前端性能优化(一) - 请求和响应优化

142 阅读10分钟

目的:

更快的内容到达时间

核心思路:

  1. 更好的传输效率
  2. 更少的请求数量
  3. 更小的资源大小
  4. 合适的缓存策略

最佳实践:

  • 减少DNS查找:

    每次主机名的解析都需要一次网络往返,增加了请求的延迟时间,还会阻塞后续请求

  • 重用TCP链接:

    尽可能使用持久连接,消除因TCP握手和慢启动导致的延迟

  • 减少HTTP重定向:

    重定向需要额外的DNS查询和TCP握手,非常耗时,最佳的重定向次数为0

  • 压缩传输的资源:

    比如Gzip、图片压缩

  • 使用缓存:

    比如HTTP缓存、CDN缓存、Service Worker缓存

  • 使用CDN(内容分发网络):

    把数据放在离用户地理位置更近的节点,可以明显减少每次TCP连接的网络延迟,增大吞吐量

  • 删除没有必要请求的资源:

    省流、省时

  • 在客户端缓存资源:

    缓存必要的应用资源,避免重复请求相同内容,比如图片

  • 压缩传输内容:

    传输数据之前先压缩,减少传输的字节,需要注意在压缩的时候确保对每种不同的资源采用最好的压缩手段

  • 消除不必要的请求开销:

    减少请求的HTTP头部数据,比如cookie

  • 并行处理请求和响应:

    请求和响应的排队都会导致延迟,可以尝试并行处理,比如利用多个HTTP1.1连接实现并行下载,在可能的情况下使用HTTP管道计数

  • 优化协议版本:

    升级到HTTP2.0

  • 采用服务端渲染:

    解决SPA应用首屏渲染慢的问题

  • 预渲染加载静态页面:

    页面渲染的极致性能,比较适合静态页面

具体优化策略

DNS解析优化

一般来说,典型的一次DNS解析需要耗费20-120毫秒,当网站资源依赖于多个不同域的时候,时间会成倍的增加,从而影响了网站的加载时间。

DNS解析时间可通过浏览器F12 - Network - Waterfall / Timing查看,查看前需清除浏览器DNS缓存和操作系统DNS缓存

在前端方面,可以通过以下两点来进行优化:

1、减少DNS的请求次数

  • 缓存DNS以提高性能,一般浏览器会默认帮我们做这件事情;

  • 减少域名的数量,可将资源划分为至少2个但不超过4个域名,这即减少了DNS查找又兼顾了并行下载效率;

2、进行DNS预获取:dns-prefetch

  • 预解析后面要加载的文件,或者用户会尝试打开的链接;

  • 默认情况下浏览器会对页面中的跨域域名进行预获取并且缓存;

  • 使用方式:<link rel="dns-prefetch" href="//xxx.xxx.xxx">

HTTP优化

1、长连接

HTTP 协议的初始版本中,每进行一次通信就要断开一次tcp连接,但是随着互联网的发展,文档中包含大量富文本的场景多了起来。

为了解决这个问题,有些浏览器在请求时,用了一个非标准字段:Connection: keep-alive来要求服务器不要关闭tcp连接,以便其他请求复用。

HTTP/1.1 中,最大的变化就是引入了持久连接(长连接):

  • 长连接:默认开启,超时自动关闭;

  • 管道机制:同一tcp连接里边,客户端可以同时发送多个请求,服务端按顺序返回响应(请求并行,响应串行);

  • Content-Length:用来区分数据包是属于哪一个响应的;

  • 分块传输编码(流模式):Transfer-Encoding: chunked

    1. 用于耗时的动态操作,产生一块数据,就发送一块;

    2. 每个非空的数据块之前,都有一个16进制的数值,表示这个块的长度;

    3. 最后一个大小为0的块,表示本次回应的数据发送完了;

  • 存在的问题:

    同一tcp连接里边,服务端是按次序处理响应的,如果前边的回应比较慢,后面就会有许多请求排队等着,造成“队头阻塞”,为了避免这个问题,只有两种方法:

    1. 减少请求数;

    2. 同时多开持久连接;

2、HTTP/2

此命名法表示不会再有子版本,下一个版本直接是HTTP/3,HTTP 2最大的变化就是二进制协议。

  • 二进制协议

    http/1.1中,头信息是文本,数据体可以是文本,可以是二进制;

    http/2中,头信息和数据体都是二进制,并且统称为 “帧” :头信息帧、数据帧;

  • 多工

    http/2中,客户端和服务端可以同时发送多个请求和响应,且不用按照顺序,避免了“队头阻塞”;

    这样双向、实时的通信,就叫做多工。 查看示例

  • 数据流

    1. 数据流ID

      http/2中,将每个请求和回应的所有数据包,称为一个数据流,每个数据流都有一个独一无二的编号(ID);

      一个tcp连接中,回应的数据包中必须标记数据流的ID,用以区分属于哪个数据流(响应);

      规定:客户端发出的数据流ID,一律为奇数,服务端发出的,为偶数;

    2. 取消数据流

      http/1.1中,只能通过断开tcp连接取消请求;

      http/2中,可以取消某一次请求(发送RST_STREAM帧),同时保持tcp的连接以便其他请求使用;

    3. 优先级

      客户端可指定数据流的优先级,优先级越高,服务器越早回应;

  • 头信息压缩

    http/2中引入了头信息压缩机制,可使用gzip、compress等压缩后再发送;

    http/1.1中,每次请求必须带上所有的cookie、User-Agent等信息,一模一样的内容重复附带,浪费带宽,也影响速度;

    http/2中,可在客户端和服务端各维护一份头信息表,根据字段生成索引号,后续只需要发送索引号即可;

  • 服务器推送

    http/2允许服务器未经请求,主动向客户端发送数据,比如静态资源的推送;

数据压缩

1、响应数据压缩

客户端使用 Accept-Encoding: gzip, deflate, br, ... 告诉服务器它支持的压缩格式;

服务端使用 Content-Encoding: gzip 告诉客户端它使用的压缩格式;

主要针对文本

2、请求数据压缩

  • 请求头信息压缩

    http/2支持头部信息压缩,http/2之前不支持;

  • 请求体数据压缩

    一般需要自定义,与服务端协商解决,常用的三种压缩方式:gzip、deflate、deflate-raw;

HTTP缓存(重要)

使用缓存能有效减少带宽浪费,减少浏览器延迟,是一种提升网站性能和用户体验的有效策略;

缓存的原理是在首次请求后保存一份响应资源的副本,当再次发起相同请求,如果判断缓存命中则拦截请求,返回之前存储的副本,从而避免向服务端发起资源请求;

浏览器缓存属于私有缓存,只能被单独的用户使用,它又细分为强制缓存和协商缓存;

二者最大的区别是,强制缓存只判断数据是否过期,协商缓存则需要询问服务器数据有更新;

强缓存

http/1.0中,使用 Expire:UTC时间格式 来判断缓存是否过期;

这样有个很大的弊端:过分依赖本地时间戳,如果客户端时间与服务端时间不同步,那么对缓存的判断就无法与预期相符;

http/1.1中,新增了 Cache-Control 字段来对Expire进行扩展和完善,常用参数如下:

  • max-age: xxx,xxx定义一个滑动时间,单位s,代表资源的过期时间;

  • no-cache: 强制进行协商缓存;

  • no-store: 禁止使用缓存策略,客户端的每次请求都需要服务端给予全新响应;

  • public: 允许代理服务器进行缓存(共享缓存);

  • private: 只允许客户端进行缓存(私有缓存),默认;

  • s-maxage: 设置代理服务器的缓存时间,需搭配public使用;

协商缓存

协商缓存需搭配 Cache-Control: no-cache 使用;

服务端在响应头设置 last-modified: UTC时间格式,之后客户端再次请求时会在请求头通过 If-Modified-Since: UTC时间格式 带入这个时间,服务端拿到后跟当前文件修改时间作对比,如果一致就返回304,客户端拿到304响应就会去缓存中取数据;

last-modified 能满足大多数场景,但也有两点不足:

  1. 如果文件只改了名字没有改内容,时间戳也会更新,容易造成带宽资源浪费;

  2. 时间戳单位是秒,如果文件在毫秒级别内完成修改,就无法验证缓存的有效性了;

http/1.1新增了一个ETag(Entity Tag 实体标签)来弥补这个不足:服务端设置 ETag: 指纹串,客服端再次请求时会通过 If-None-Match 带入,服务端拿到后对比并返回相应状态码;

指纹串可根据内容或指定属性生成,可使用第三方包

ETag虽然补充了last-modified的不足,但它也存在一些弊端:

  1. 如果资源的尺寸大、数量多、修改频繁,生成ETag的过程会影响服务器的性能;

  2. ETag分强验证和弱验证,弱验证根据资源部分属性生成,无法保证每个字节都相同,特别是在服务器集群场景下,无法保证协商缓存验证的准确率;

缓存如何决策

我们既希望缓存能在客户端尽可能久的保存,又希望它能在资源发生修改时及时进行更新,如何解决这两个互斥的优化诉求?

我们可以将网站所需的资源按照不同的类型制定不同的缓存策略:

  • HTML文件包含了其他文件,需要保证其内容发生修改时能及时更新,建议使用协商缓存 no-cache;

  • 图片资源一般都是修改,且考虑到数量和大小对缓存空间的影响,可采用强制缓存且过期时间不宜过长 max-age=86400;

  • 文本文件可能存在内容的不定期修改,可考虑增加文件指纹(比如打包命名的hash串),文件名修改导致请求的URL不同,必然会对资源重新请求保证其时效性。同时为了兼顾重用效率,考虑到浏览器和CDN等中间代理的缓存,其过期时间可适当延长到一年,即 public, max-age=31536000;

  • 包含用户敏感信息的脚本文件,为防止代理服务器缓存,可设置 private;

CDN缓存

即内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、调度及内容分发等,使用户在请求内容时能就近获取,以此来降低阻塞,提高响应速度。

CDN主要针对静态资源的存放,一般需要前后端配合来进行优化。

这里放一个示例:主站请求的域名为 www.xxx.com ,静态资源请求CDN服务器的域名可以用 a.xxx.com 和 b.xxx.com,设计成与主站域名不同的原因主要有两点:

  1. 避免对静态资源的请求携带不必要的cookie信息;

  2. 浏览器对同一域名下并发请求的限制;

要注意对同一资源使用不同域名请求的问题,这样会导致缓存无法重用。