HTTP缓存

一.分类

按缓存的强势程度分为:

  • 强缓存:有效期内,资源直接从本地缓存取(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-ageExpires没有过期,就能命中强缓存

协商缓存

缓存过期之后,再次访问该资源,浏览器会带上本地缓存版本号去询问server,server检查客户端递过来的ETagLast-Modified值,告诉客户端要不要更新缓存

响应头中的ETagLast-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-SinceIf-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=0Pragma:no-cache的具体行为依赖server实现,实际上代理服务器不一定会回源或者检查过期

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code