常见http缓存

记录下http常见缓存,比如常说的协商缓存,强缓存 …

概述

缓存的种类可分为两大类,私有缓存共享缓存

  • 共享缓存:共享缓存可以被多个用户使用,比如为特定的用户提供热门资源缓存,缓存服务器等。
  • 私有缓存:私有缓存只能用于单独用户,这里主要介绍浏览器http缓存

http缓存就是避免相同的资源反复加载,用来提高web程序的响应性能。

常见的HTTP缓存只能存储 GET 响应,缓存内容由请求地址(URI)和方法(request method),针对一些特定的请求,也可以通过关键字区分多个存储的不同响应以组成缓存的内容,参考:Vary

按照日常使用,把缓存配置分为 强缓存协商缓存 逐个介绍,如下。

强缓存包括: ExpiresCache-control
协商缓存包括: ETagLast-Modified

Cache-control

Cache-controlHTTP1.1 配置,请求头和响应头都支持这个属性。

主要包含如下关键字配置:

可缓存性

no-store

  • 禁止缓存数据到本地

no-store 不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

需要完全禁用缓存的时候设置 no-store 即可。

no-cache

  • 缓存但重新验证

no-cache 响应可以存储数据到本地,但使用缓存前需要和源服务器校验缓存有效性(协商缓存验证)。

如果在浏览器(非支持bfCache浏览器)前进后退时,页面资源请求也会直接使用已有缓存,no-store 没存储缓存所以并不会。

public

  • 可以被任何缓存器缓存

public 表明响应可以被任何缓存器缓存比如(代理服务器,cdn等),即使是通常不可缓存的内容

private

  • 只能被是有缓存器缓存
    private 也就是只能被浏览器缓存。

如果不想被 任何缓存器缓存需要使用 no-store

过期时间

max-age

  • 资源被视为新鲜的最长时间

通过 max-age=<seconds> 设置缓存有效时间(最大新鲜时间),通常应用于页面中不可更改的文件,比如js 、css、图片等静态资源。

Expires 指令不同,该指令是相对于请求时间的差值

s-maxage

max-age 一样表示缓存有效时间,优先级高于 覆盖 max-age 或 Expires,但仅适用于共享缓存(例如,代理、cdn等),会被私有缓存忽略。

max-stale

表明客户端愿意接收一个已经过期的资源,设置一个秒值,表示可接受最大过期时间。

为缓存续命,相当于 fresh-time(缓存时间) = max-age + max-stale

min-fresh

表示客户端希望获取一个能在指定的秒数内依旧保持其最新状态的响应。

为缓存减命,相当于 fresh-time(缓存时间) = max-age - min-fresh

stale-while-revalidate

  • 扩展指令

表明客户端愿意接受陈旧的响应,同时在后台异步重新校验缓存。设置一个秒值,表示可接受最大过期时间。

例:

1
Cache-Control: max-age=600, stale-while-revalidate=30

表示缓存在600s内新鲜,如果过期不超过30s,则缓存仍然可使用并且同时去和服务器重新校验,超过30s之后 真正 时效,需要再次和服务器重新校验。

保证资源尽量使用缓存的同时,又保证了缓存最多一次的延后,Workbox 也提供该缓存策略

stale-if-error

  • 扩展指令

表示缓存校验失败,则客户愿意接受陈旧的响应。设置一个秒值,表示可接受最大过期时间。

重新校验生效

must-revalidate

指示一旦缓存过期,就必须与源服务器校验缓存有效性,必须校验之后才能使用。

看到这里是不是挺疑惑的,缓存本来就是要校验有效性,这里为啥还需要这样一个属性?

must-revalidate 生效有个前提,就是这个缓存必须已经过期,缓存过期之后就会重新 revalidate 为啥还需要一个 must-revalidate,这是因为http规范在某些特殊情况可以直接使用过期的缓存,比如(stale-while-revalidatestale-if-error 等)

带有 must-revalidate 的缓存,在任何情况下,都必须成功 revalidate 后才能使用,没有例外。

参见这里,详细解释了为啥需要 must-revalidatehttps://zhuanlan.zhihu.com/p/60357719

proxy-revalidate

类似于 must-revalidate,但仅用于共享缓存(例如代理)。被私有缓存忽略。

immutable

  • 扩展指令
    表示响应正文不会随时间而改变,该资源(如果未过期)在服务器上不变,因此,即使用户显式刷新页面,客户端也不应为其发送条件重新验证。

Firefox 要求必须为https

Pragma

PragmaHTTP/1.0 支持配置。

只支持 no-cache 配置,如果不配置 Cache-Control,行为与 Cache-Control: no-cache 一致,主要在一些需要兼容 HTTP/1.0 的场景中使用。

例如:Chrome 强制刷新浏览器

1
2
Cache-Control: no-cache
Pragma: no-cache

Expires

HTTP1.0 支持配置。

缓存过期时间,如果 Cache-Control 配置了 max-age 或者 s-max-age 那么 Expires 将被忽略。

例:

1
Expires: Wed, 21 Oct 2015 07:28:00 GMT

使用 GMT 时间格式,且时间会受到客户端时间的影响,所以假如用户本地时间有误可能会导致缓存异常,max-age 使用时间差值相隔秒数,避免了这种问题

Last-Modified

HTTP1.0 支持配置。

response 头,内容为服务器资源修改的日期及时间,使用 GMT 时间格式

当请求过服务器之后,服务器返回了 Last-Modified 头,下次需要重新校验的时候浏览器会发送 If-Modified-Since (当前缓存的最后修改时间)请求头。

然后服务器对比 If-Modified-Since 时间和当前资源的 Last-Modified ,看文件是否有变更,变更过会返回 200 重新接收新资源和缓存策略,否则没变更过返回 304 直接使用当前缓存。

使用 Last-Modified 存在一些不能很好使用缓存的缺陷:

  1. 如果资源是服务端动态输出的非静态资源那修改时间就永远等于当前时间了,或者定期的重新生成内容,CDN多个节点时间不一致等,都会导致更新时间变更
  2. 时间控制的精度是只能到秒级

Etag 有效避免了这些问题

启发式缓存

条件:如果 max-ageexpires 属性都没有,但存在 Last-Modified

缓存的新鲜时间就等于头里面 Date 的值减去 Last-Modified 的值除以10(注:根据rfc2626其实也就是乘以10%),其实就是 文件最近一次更新到现在的十分之一时长作为可缓存时长

也就是 (date_value - last_modified_value) * 0.10,但这个只是推荐的规范,各个浏览器的实现可能会有差别。

如下:Chrome 中触发启发式缓存,没有设置强缓存也会响应 200 form disk cache

lastmodified-date

Cache-Control: no-cache 或者 max-age=0 可以禁用启发式缓存,但在 Chrome 中,Cache-Control: must-revalidate 也有同样的功能。

Etag

HTTP1.1 支持配置。

EtagLast-Modified 类似,区别在于判断文件的变更不是使用变更时间,而是通过内容指纹,来解决了 Last-Modified 的缺陷。

如果同时设置 EtagLast-Modified 那需要同时满足新鲜度校验才会返回 304

Vary

Vary 是一个HTTP响应头部信息,它决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复。
它被服务器用来表明在 content negotiation algorithm(内容协商算法)中选择一个资源代表的时候应该使用哪些头部信息(headers)

通俗点讲:

首先常见HTTP缓存只能存储 GET 响应,缓存内容由请求地址(URI)和方法(request method)确定,如果需要一个 URI 提供不同的内容,那就需要一个 key 来区别标识,这个就是通过 Vary 来配置。

通过 Vary 可以针对一个请求地址(URI)来提供多份内容。只有当 Vary 配置的头,在当前请求和原请求缓存都匹配时,才可以使用缓存,这个过程就是 content negotiation(内容协商)

lastmodified-date

  1. Client1 请求缓存服务器 Accept-Encoding = *, 没有缓存再继续请求源服务器
  2. Client2 请求缓存服务器 Accept-Encoding = br,缓存不匹配,请求源服务器返回了 Vary: Accept-Encoding,就是告诉缓存服务器下次相同的请求也使用 Accept-Encoding = br 请求头可以使用缓存
  3. Client3 请求缓存服务器 Accept-Encoding = br ,命中了缓存,使用缓存返回给客户端而不再请求源站

例如:

1
2
3
Vary: <header-name>, <header-name>, ...

vary: Accept-Encoding,Accept-Language,User-Agent

Vary: * 表示总是不匹配,请求都会到达源站服务器。

对于浏览器来说,浏览器并不会对 Vary 存储多个副本,而只是通过 Vary 做缓存校验

CORS和Vary

如果服务器 Access-Control-Allow-Origin 未使用 *,而是指定了一个域,那么为了向客户端表明服务器的返回会根据 Origin 请求头而有所不同,必须在 Vary 响应头中包含Origin。

1
2
Access-Control-Allow-Origin: https://developer.mozilla.org
Vary: Origin

浏览器行为

Command + R

Chrome、firefox 都会在页面 html文档request 请求头加上 Cache-Control max-age=0,也就是重新校验缓存。

但: firefox 不仅会对 html 文件加max-age=0,也会对页面内的资源请求加 max-age=0

Command + Shift + R

firefox、Chrome 都使用request 请求头加上 Cache-Control: no-cachePragma: no-cache 来强制重新请求服务器。

区别:

  • Chrome - 只会对部分资源添加 no-cache

体验上像是只对在页面 load 之前所有的请求加上如上参数,其它的 异步资源(包含:xhr请求,manifest.json,图片延迟加载,onload事件之后导入的异步脚本等等) HTTP2 Server Push 资源 都不会添加继续使用缓存。

  • firefox - 会绝大部分资源都加上 no-cache

体验上看也有例外,比如:HTTP2 Server Push 资源manifest.json内的图片请求还是会使用缓存,同时有些在 onload 之后加载的脚本也会使用缓存

network disabled cache

浏览器控制台禁用缓存,类似强制刷新,但对所有资源都会请求都会加上 no-cache,包括 PWA manifestHTTP2 Server Push异步资源 等等

这里 Chrome 和 firefox 表现一致。

浏览器前进/后退

支持 bfCache 的浏览器会永远使用缓存且脚本不重新执行。
不支持的除非配置 no-store 否则也是使用缓存。

其它

bfCache

当你浏览器前进后退页面完全不刷新,且js的执行状态都是保持的,这个特性就是 BF Cache,全称 Backward/Forward Cache,可以在 safari 浏览器中体验,Chrome 目前默认不开启这个。

由于不刷新且完全保留上个页面的js执行状态,所以页面绝对缓存,即使 no-store

可以通过 window pageshow 事件来监听页面前进后退行为。

另外在 Chrome浏览器 中前进后退的操作时,页面资源也不会重新请求,除非显示配置了 Cache-control: no-store

max-age=0 vs no-cache 区别

  • responseno-cachemax-age=0,以及 request 中的 max-age=0 都是一样的,表示缓存过期(不新鲜)需要重新进行校验。
  • request 中的 no-cache 则表示要重新请求服务器资源,如:浏览器在强制刷新的时候使用该方式强制请求服务器资源。

参考