Web 性能优化:HTTP
- Web 性能优化:TCP
- Web 性能优化:TLS
- ⚓ Web 性能优化:HTTP
以 Web 应用为代表的互联网肇始于 1991 年 Tim Berners-Lee 提出的 HTTP/0.9 协议,经过 30 多年的发展,伴随着 IT 巨头的明争暗斗,已经逐渐渗透进我们生活的方方面面,成为当代社会不可或缺的组成部分。
HTTP 协议的历史
1991 年,HTTP(HyperText Transfer Protocol,超文本传输协议)第一个版本 — HTTP/0.9,诞生了,功能比较简陋,设计目标包括:
- 支持文件传输;
- 能够通过索引搜索 HTML;
- 格式化协商机制;
- 能够把客户端引导到不同的服务器。
HTTP/0.9 奠定了整个互联网交互的大框架:
- 客户端(请求)—服务器(响应)模式;
- 基于 TCP/IP 协议栈。
- 在 HTTP/0.9 版本中,TCP 连接在每次 HTTP 请求后都会关闭。
1996 年,HTTP/1.0 版本(rfc1945)发布,主要是参考性质的,总结了一些最佳实践和模式,增加了内容协商,响应对象不再局限于 HTML,彼时一个请求仍然对应一个 TCP 连接。
1997 年,HTTP/1.1 版本(rfc2068)正式发布,厘清了之前版本中很多有歧义的地方,并对性能进行了优化:
- 持久连接,复用 TCP 连接;
- 分块编码传输;
- 字节范围请求;
- 增强的缓存机制;
- 传输编码;
- 请求管道,不用等待前一个请求回应就发送下一个请求,支持的不多。
2015 年,HTTP/2 版本(rfc7540)发布,主要聚焦性能,将原来基于文本传输的模式改为基于二进制数据,在传输层之上引入二进制分帧数据层,每个【主机:端口】组合仅一个持久的 TCP 连接,在该连接上进行多路复用。
2022 年,HTTP/3 版本(rfc9114)发布,使用的传输层协议从 TCP 改为 UDP。
从 HTTP/0.9 到 HTTP/3,针对不同的场景和需求,协议在不断地进化,但现实世界中难以对大量的软硬件进行统一升级,所以无法做到一键切换,新旧协议的交替往往需要持续一段相当长的时间(一般 5-10 年),所以在一段时间里,客户端、代理、服务器仍然需要同时支持多种版本的 HTTP 协议。
性能优化
对 Web 应用来说,性能优化可以简要描述为:如何更快地完成用户请求。
个人理解的核心原则是:(在大多数时候)尽量减少网络请求。—— 有点像车轱辘话 🤔。
具体手段有:
1、使用压缩
通过在请求中声明标头【Accept-Encoding: <压缩算法>】来和服务端协商对 body 进行压缩后传输,压缩算法有 gzip、deflate、compress、br,一般选用 gzip。如果服务端同意,后续响应中就对 body 进行压缩,并且响应中包含标头【Content-Encoding: <压缩算法>】。
使用压缩可以减少需要传输的数据量,从而减少了需要传输的网络包,增加了两端的处理时间,减少了中间的传输时间,属于以时间(两端的 CPU 时间)换空间(传输数据量)的优化策略。
2、CDN
将资源服务器托管到靠近用户的网络运营商处,用户请求时直接请求 CDN,减少了网络跳数,保证网络延迟最小。
3、条件式请求
当资源过期后,通过传递资源的版本标识,询问服务端该缓存资源是否可用。如果可用的话,服务端返回【304 Not Modified】,就成功避免了一次多余的数据响应,浏览器会根据场景调整缓存的有效性。
资源有两种标识方式:
- Last-Modified: <date/time>,最后修改时间;
- Etag:
,版本号。
服务端返回资源时携带资源标识。
当请求中包含以下标头时,就是条件式请求:
- If-Match: <etag_value>,版本号匹配时才执行;
- If-None-Match: <etag_value>,版本号不匹配时才执行;
- If-Modified-Since:
, : : GMT,在此日期之后修改过才执行; - If-Unmodified-Since:
, : : GMT,在此日期之前修改过才执行; - If-Range:
, : : GMT,用于断点续传; - If-Range: <etag_value>,用于断点续传。
和 PUT 方法一起用时,就是乐观锁的思路。
4、Expires
当服务端第一次返回资源时,设置标头【Expires: <有效时间>】,该资源缓存在客户端,在有效时间内,资源都是 fresh 状态的,可以直接取用,当过期以后,资源就变为 stale 状态,需要重新和服务端协商获取。
5、Cache-Control
HTTP 协议中定义了两类缓存:
- 私有缓存,特定用户所有,通常放在浏览器端;
- 共享缓存,可以在多个用户之间共享,可以放在路径上的代理、CDN、反向代理、浏览器端。
可以通过标头 Cache-Control 来控制缓存策略。
Cache-Control 的指令可以组合适用(通过逗号分隔),可选指令包含:
- public,声明为共享缓存;
- private,声明为私有缓存,只能被用户浏览器缓存;
- no-cache,在用缓存进行响应前,必须把请求提交给服务器验证有效性;
- no-store,禁用缓存;
- max-age=<秒>,缓存到期时间;
- s-maxage=<秒>,覆盖 max-aqge 或 Expires 头,仅适用共享缓存;
- must-revalidate,资源过期后必须向服务器验证,否则不能作为响应;
- proxy-revalidate,共享缓存中的资源过期后必须向服务器验证,否则不能提供;
- no-transform,不得对资源进行转换或转变,如代理不能修改 Content-Encoding、Content-Range、Content-Type 标头。
6、持久连接
在 HTTP/1.1 之前,一个 HTTP 请求会申请一个 TCP 连接,请求结束即关闭连接,下一个请求需要重新申请 TCP 连接。TCP 连接的建立是通过三次握手,也就是每个请求至少需要延迟一个网络往返时间才能得到响应,而这部分延迟是可以避免的。
于是在 HTTP/1.1 中引入了持久连接,一个请求结束后并不会立即关闭该 TCP 连接,允许其他 HTTP 请求继续通过它发送数据。在 HTTP/1.1 中,默认情况下 TCP 连接是持久的,只是有些程序库(出于简化编程模型的目的)在实现时会简单新发起一个 TCP 连接。持久连接可以通过设置标头【Connection: close】来关闭。所谓的持久连接,就是指保持传输层的 TCP 连接持久。日常一般简称长连接。
7、避免重定向
重定向通常会引起一次 DNS 解析和一次 TCP 三次握手,增加了一定的延迟。
8、合并静态文件
由于目前的网页越做越大,一个网页通常包含很多静态文件(JS、CSS、Image、...),资源越多,请求数量越多(资源路径不同、HTTP/1.0 不支持持久连接),可以将多个静态资源合并为少量的几个,这样 HTTP 请求也相应地减少了。
会引入一些问题,如当某个静态文件发生变化时,需要重传整个大的静态文件;当某个网页只需要部分静态资源时,却必须请求整个静态文件,多了很多不需要的数据。
9、域名分区
在 HTTP/1.x 协议中,要求浏览器针对每个【域名+端口】维护一个连接池,每个连接池最多包含 6 个 TCP 连接,如果不够用,服务端可以设计不同的【域名+端口】来分配资源,以突破浏览器端 6 个 TCP 连接的限制,提高传输效率。
10、多个 TCP 连接
在 HTTP/1.x 中不支持多路复用,可以创建多个 TCP 连接,并行请求服务端资源。多个 TCP 连接也可以避免单个 TCP 持久连接中的队首阻塞问题(因为 tcp 保障按序传输,发送序列中的某一个分组传输失败,后续分组到达接收端后只能保存在 tcp 缓冲区中,等待失败的分组重发并到达后才能交付给应用程序)。
11、升级到 HTTP/2
http/2 引入多路复用、头部压缩功能和流优先级等新特性。
多路复用是将不同的 http 请求当作不同的流,每个流由若干帧组成,不同流的帧可以混杂放到一个持久连接中,从而达到多个请求共用一个 TCP 持久连接的效果。
HTTP/2 引入了头部压缩功能,在客户端和服务端保持一个头部表来跟踪头部的键值对。能减少需要传输的数据量(如 Cookie、User-Agent)。
在 HTTP/2 中,一个【域名+端口】只允许建立一个 TCP 连接,且这个连接是持久的。
一般 HTTP/2 会直接搭配 TLS 使用,如在 nginx 中就可以在 ssl 后配 http2。
这种模式没有解决队头阻塞问题,所以也为 HTTP/3 留下了想象的空间。
12、Server Push
HTTP/2 引入的新特性。发送一个请求后,服务端推送多个响应。但服务端不能任意推送响应,必须是在第一个(initial)请求中包含的特定链接所指向的内容。Push 是服务端到客户端的一个单向通道。
参考:
- 《Web 性能权威指南》
- 《HTTP/2 in Action》
- MDN