针对浏览器的http缓存的分析也算是老生常谈了,每隔一段时间就会冒出一篇不错的文章,其原理也是各大公司面试时几乎必考的问题。
之所以还写一篇这样的文章,是因为近期都在搞新技术,想“回归”下基础,也希望尽量总结的更详尽些。
基本原理
- 浏览器在加载资源时,根据请求头的expires和cache-control判断是否命中强缓存,是则直接从缓存读取资源,不会发请求到服务器。
- 如果没有命中强缓存,浏览器一定会发送一个请求到服务器,通过last-modified和etag验证资源是否命中协商缓存,如果命中,服务器会将这个请求返回,但是不会返回这个资源的数据,依然是从缓存中读取资源
- 如果前面两者都没有命中,直接从服务器加载资源
http报文中与缓存相关的首部字段
- 通用首部字段:
Cache-Control,Pragma - 请求首部字段:
if-Match,If-None-Match,If-Modified-Since,If-Unmodified-Since - 响应首部字段:
Etag - 实体首部字段:
Expires,Last-Modified
缓存的分类
- 强缓存
- 协商缓存
强缓存和协商缓存的异同点
- 相同点:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据
- 不同点:强缓存不发请求到服务器,协商缓存会发请求到服务器。
强缓存
强缓存通过Expires和Cache-Control两种响应头实现
- Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个
绝对时间,由服务器返回。Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效
Expires: Wed, 11 May 2018 07:20:00 GMT
- Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires ,表示的是
相对时间
Cache-Control: max-age=315360000
- Cache-Control属性介绍
-
可缓存性
private,响应指令:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。no-cache,请求指令和响应指令: 不会缓存到本地的说法是错误的。实际上是可以缓存到本地缓存区中的,每次使用缓存资源前都必须重新验证其有效性,在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。no-store,请求指令和响应指令:不缓存请求或响应的任何内容public,响应指令: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。)-
到期
max-age=<seconds>,请求指令和响应指令: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。s-maxage=<seconds>,响应指令: 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。max-stale[=<seconds>],请求指令: 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。min-fresh=<seconds>,请求指令: 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。stale-while-revalidate=<seconds>,拓展指令不是http缓存标准的一部分: 表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度。stale-if-error=<seconds>,拓展指令不是http缓存标准的一部分: 表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间。-
重新验证和重新加载
must-revalidate,响应指令: 一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。proxy-revalidate,响应指令: 与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。immutable,拓展指令不是http缓存标准的一部分: 表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或If-Modified-Since)来检查更新,即使用户显式地刷新页面。在Firefox中,immutable只能被用在 https:// transactions-
其他
no-transform,请求指令和响应指令: 不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等HTTP头不能由代理修改。例如,非透明代理或者如Google's Light Mode可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform指令不允许这样做。only-if-cached,请求指令: 表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。
-

协商缓存
当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串
1. Last-Modified,If-Modified-Since
Last-Modified 表示本地文件最后修改日期,浏览器会在request header加上If-Modified-Since(上次返回的Last-Modified的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
2.ETag、If-None-Match
Etag就像一个指纹,资源变化都会导致ETag变化,跟最后修改时间没有关系,ETag可以保证每一个资源是唯一的
If-None-Match的header会将上次返回的Etag发送给服务器,询问该资源的Etag是否有更新,有变动就会发送新的资源回来

具体为什么要用ETag,主要出于下面几种情况考虑:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET; 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒); 某些服务器不能精确的得到文件的最后修改时间。
整体流程图

缓存集中状态码
- 200: 强缓Expires/Cache-Control存失效时,返回新的资源文件
- 200: from disk cache(磁盘缓存)和from memory cache(内存缓存)
- 304(Not Modified ):协商缓存Last-modified/Etag没有过期时,服务端返回状态码304
如何选择合适的缓存
协商缓存需要配合强缓存使用,如果不启用强缓存的话,协商缓存根本没有意义 大部分web服务器都默认开启协商缓存,而且是同时启用【Last-Modified,If-Modified-Since】和【ETag、If-None-Match】
但是下面的场景需要注意:
分布式系统里多台机器间文件的Last-Modified必须保持一致,以免负载均衡到不同机器导致比对失败; 分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样)
参考
HTTP MDN 和 amandakelake