一.分类
按缓存的强势程度分为:
强缓存:有效期内,资源直接从本地缓存取(disk cache或memory cache);有效期外或强制刷新时,找server再要一份
协商缓存:有效期内,同上;有效期外或强制刷新时,带着本地版本号询问server资源是否有更新,得到回复
304
(更新过期时间等缓存状态,接着用本地版本)或200
(把新版本缓存起来,本地版本扔掉)
其中,协商缓存可以细分为:
基于时间的:以资源修改时间(Last-Modified)为版本号
基于内容的:以资源内容hash(ETag)为版本号
协商是缓存失效(过期或弃用)之后才会发生的事情
二.相关Header字段
HTTP Header字段分为4类:
general-header(通用头):同时适用于请求和响应消息
request-header(请求头):允许client传递额外的信息给server,请求修饰符,作用相当于参数
response-header(响应头):允许server传递关于该响应的额外信息给client,额外信息包括server相关的,以及将来访问该资源需要的一些信息
entity-header(实体头):给出消息实体相关的meta信息,如果没有消息实体的话,就是与请求对应的资源的信息
P.S.关于HTTP Header的更多信息,请查看4.2 Message Headers
Pragma
HTTP 1.0通用头字段,指定缓存策略
Pragma = "Pragma" ":" 1#pragma-directive
pragma-directive = "no-cache" | extension-pragma
extension-pragma = token [ "=" ( token | quoted-string ) ]
Pragma是一个含义模糊的字段,RFC仅指定了Pragma: no-cache
出现在请求中时,即便缓存有效,也应该回源去取新的,与Cache-Control: no-cache
等价。出现在响应中时,没有明确含义
P.S.关于Pragma的更多信息,请查看14.32 Pragma
Expires
HTTP 1.0实体头字段,表示资源的过期时间,指定过期策略
Expires = "Expires" ":" HTTP-date
一个精确的时间点,在此之前,缓存有效。这个时间点由server给出,如果client与server的时间不同步,缓存过期策略就不可靠了
无法保证Expires
给出的时间点在client和srever对应同一个时刻,所以HTTP 1.1新增了可以通过Cache-Control: max-age=<seconds>
来定义保质期,给一个时间段,从client拿到资源后,再过seconds
秒缓存过期,这样就只依赖client时间,不要求一致性了
Cache-Control
通用头字段,指定缓存策略和过期策略
Cache-Control = "Cache-Control" ":" 1#cache-directive
cache-directive = cache-request-directive
| cache-response-directive
cache-extension = token [ "=" ( token | quoted-string ) ]
响应头中可以出现9个值:
cache-response-directive =
; 资源将被客户端和代理服务器缓存
"public"
; 资源仅被客户端缓存,不允许代理服务器缓存
| "private" [ "=" <"> 1#field-name <"> ]
; 不先回源检查的话,不允许复用资源
| "no-cache" [ "=" <"> 1#field-name <"> ]
; 资源不允许被写入缓存
| "no-store"
; 禁止代理服务器修改Content-Encoding,Content-Range,Content-Type字段
| "no-transform"
; 不允许使用过期的资源,一旦过期,必须回源验证(即使客户端愿意接受过期资源)
| "must-revalidate"
; 依赖public,类似于must-revalidate,仅适用于代理服务器
| "proxy-revalidate"
; 缓存资源,但是在指定时间(单位为秒)后缓存过期
| "max-age" "=" delta-seconds
; 依赖public,只在代理服务器上有效,覆盖max-age
| "s-maxage" "=" delta-seconds
; 自定义扩展值
| cache-extension
请求头中可以出现7个值:
cache-request-directive =
; 强制回源,不要来自缓存的内容
"no-cache"
; 不允许把客户端请求相关信息写入缓存
| "no-store"
; 客户端愿意接受age(代理服务器缓存时间)不超过delta秒的资源
| "max-age" "=" delta-seconds
; 客户端愿意接受过期delta秒内的旧内容
| "max-stale" [ "=" delta-seconds ]
; 客户端希望响应内容在delta秒内都是有效的
| "min-fresh" "=" delta-seconds
; 客户端不接受经过转换的内容,例如Content-Type
| "no-transform"
; 客户端只想要已缓存的资源,不重新请求资源
| "only-if-cached"
; 自定义扩展值
| cache-extension
注意no-store, no-cache, must-revalidate
描述间的细微差异,同一字段出现在请求头和响应头中的含义也都不同
Last-Modified
实体头字段,表示资源的最后修改时间,指定协商策略
Last-Modified = "Last-Modified" ":" HTTP-date
客户端拿到之后会保存起来,下一次向server请求资源时,会带上这个时间点作为版本号,验证本地缓存资源是否仍然可用
资源被修改过,但内容没变的话,发一份内容一样的响应就显得多余了,所以也提供了基于内容的协商缓存,避免这种情况
P.S.优先级低于Cache-Control: max-age
,同时出现时,以max-age
为准
If-Modified-Since
请求头字段,基于时间的协商策略实现需要,比较资源最后修改时间(Last-Modified,资源最后修改时间)是否一致
If-Modified-Since = "If-Modified-Since" ":" HTTP-date
把Last-Modified
版本号作为字段值发回给server,资源没更新就返回304不给响应体,更新了就返回200,把新版本内容作为响应体
If-Unmodified-Since
同上,行为相反(比较资源最后修改时间是否不一致),如果不一致并且method为POST/PUT等更新操作时,返回412(Precondition Failed,条件不满足)表示更新执行失败
ETag
响应头字段,表示资源的内容hash,指定协商策略
ETag = "ETag" ":" entity-tag
客户端会记下这个值,下一次请求该资源时作为版本号传回给server
P.S.ETag
优先级比Last-Modified
高
If-Match
请求头字段,基于内容的协商策略实现需要,比较该字段的值(ETag,资源内容hash)是否一致
If-Match = "If-Match" ":" ( "*" | 1#entity-tag )
如果不一致,并且method为POST/PUT等更新操作时,返回412表示更新失败
If-None-Match
同上,行为相反(比较该字段的值是否不一致),如果一致,返回304告诉客户端可以沿用缓存版本,否则返回新资源
Age
响应头字段,表示资源在代理服务器上已缓存的时间
Age = "Age" ":" age-value
age-value = delta-seconds
计算方式为:
/*
* age_value
* is the value of Age: header received by the cache with
* this response.
* date_value
* is the value of the origin server's Date: header
* request_time
* is the (local) time when the cache made the request
* that resulted in this cached response
* response_time
* is the (local) time when the cache received the
* response
* now
* is the current (local) time
*/
apparent_age = max(0, response_time - date_value);
corrected_received_age = max(apparent_age, age_value);
response_delay = response_time - request_time;
corrected_initial_age = corrected_received_age + response_delay;
resident_time = now - response_time;
current_age = corrected_initial_age + resident_time;
Age:0
表示刚从源server取过来,正值表示上次从源取过来到现在经过的秒数
三.强缓存与协商缓存
分别发生在缓存的不同阶段,缓存生效时走强缓存,不发请求,缓存失效后才走协商缓存,发请求询问资源更新与否
强缓存
响应内容命中强缓存后,缓存有效期内,浏览器不会向server发起请求,而是直接从本地缓存(disk cache或memory cache)读取
只要本地有该资源的缓存版本,并且Cache-Control: max-age
或Expires
没有过期,就能命中强缓存
协商缓存
缓存过期之后,再次访问该资源,浏览器会带上本地缓存版本号去询问server,server检查客户端递过来的ETag
或Last-Modified
值,告诉客户端要不要更新缓存
响应头中的ETag
和Last-Modified
是协商缓存的开关,协商缓存的好处是内容没变的话,直接返回304,不用传输响应体
四.启发式缓存
一种比较特殊的情况是响应头没有提供任何缓存相关的信息,此时浏览器会使用一个启发式算法来确定资源缓存期限:
max-age = Date - Last-Modified / 10
默认的缓存策略,就叫启发式缓存,启发式是说基于经验构造的,没有严格的依据
五.刷新行为
浏览器有3种不同的刷新行为,在验证HTTP缓存时很容易被迷惑:
开新页面:打开新tab或者窗口,访问页面
普通刷新:点击刷新按钮、地址栏回车、
CMD + R
强制刷新:
CMD + Shift + R
、Chrome长按刷新按钮,选择硬性重新加载禁用缓存再刷新:勾选
Disable cache
设置,再开新页面/刷新
开新页面
请求头不带缓存相关字段,如果本地缓存版本有效,从缓存读取,不发请求,并显示个假请求头:
Request Headers
Provisional headers are shown
Upgrade-Insecure-Requests:1
User-Agent:...
响应头沿用缓存的那份
普通刷新
不从缓存取,一定会向服务发起请求,请求头会带上If-Modified-Since
和If-None-Match
等缓存头(如果有的话),此外还会擅自添上:
Cache-Control:max-age=0
要求代理服务器检查缓存是否过期
P.S.普通刷新行为发生时,浏览器一定会发起请求,即便资源缓存仍然有效,理应处于强缓存状态。因为用户要求刷新内容,希望看到新的,而关联的资源(比如该页面含有的CSS,JS等资源)不会被强制发起请求
强制刷新
同样会强制发起请求,带上缓存相关信息,还会擅自添上:
Cache-Control:max-age=0
Pragma:no-cache
要求回源去取新的,即便缓存没过期
禁用缓存再刷新
禁用缓存后,后续所有请求都会被添上:
Cache-Control:max-age=0
Pragma:no-cache
相当于全都走强制刷新,包括关联资源
P.S.Cache-Control:max-age=0
,Pragma:no-cache
的具体行为依赖server实现,实际上代理服务器不一定会回源或者检查过期
参考资料
浏览器缓存机制剖析:缓存机制流程图不错,Header字段含义描述不正确
HTTP缓存控制小结:内容很准确,且较全面
What heuristics do browsers use to cache resources not explicitly set to be cachable?