HTTP缓存

Cache-Control

Cache-Control是HTTP1.1中引入的一种通用头部字段,可以用于请求和响应头部;现代浏览器都支持Cache-Control,主要用于替代HTTP1.0中定义的一些相应缓存标头,如Expires、Pragma等;客户端和服务端的请求头有一些差异:

指令 客户端 服务端
no-cache
no-store
max-age=<seconds>
no-transform
public ×
private ×
max-stale ×
only-if-cached ×
must-revalidate ×
proxy-revalidate ×
s-maxage=<seconds> ×
  1. no-cacheno-store
    no-store禁止浏览器及所有的中间件(代理服务器等)缓存任何版本的响应信息,即我们通常所说的不缓存,每次请求都需要向服务器发送完整地请求并下载相应的响应;

    no-cache不直接使用缓存,需要先在与服务器确认返回的响应是否发生变化。即如果存在ETag(如ETag: af47a1d)等验证令牌时,会发送请求(请求带着If-None-Match: af47a1d标头)到服务器检验之前的缓存内容和当前版本有无变化,如果无变化则返回响应状态码304,否则重新请求并下载资源;

  2. publicprivate
    private:响应仅对单个用户进行缓存,也就是说对于代理服务器、CDN等中间缓存机构不进行缓存;
    public:非必须项,表示响应在多数情况下均可以被缓存(包括CDN、代理服务器等);

  3. only-if-cached:客户端标头,表示客户端仅接受已缓存的响应,并且不会检测当前响应的新鲜度。

  4. max-age: <seconds>:从请求时间开始计算的缓存存储的最大有效时长,单位为秒;超过则为过期;

  5. max-stale[=<seconds>]:客户端标头,表示客户端可以接受一个过期资源;如果设置了可选的时间参数,则表示过期时间不超过该时长;

  6. must-revalidate:服务端标头,使用缓存前需要验证旧资源的状态,过期资源不可用;

  7. proxy-revalidate:服务端标头,仅适用于共享缓存(如代理等),作用与上类似;

// 禁止缓存
Cache-Control: no-cache, no-store, must-revalidate	

// 缓存静态文件
Cache-Control: public, max-age=31536000

下图是MDN推荐的最佳Cache-Control策略:

HTTP缓存

Pragma

Pragma是HTTP1.0中定义的通用首部字段,与HTTP1.0中的Expires标头共同决定HTTP1.0中的缓存策略;Pragma: no-cache作用与Cache-Control: no-cache作用一致;现在通常用与对HTTP1.0客户端的兼容;

Expires

Expires是HTTP1.0及HTTP1.1都有的响应头部字段,表示启用缓存并设置资源过期的时间点,包含日期、时间;设置过去的时间则表示资源过期;同时设置Pragma和Expires则不缓存;这里需要注意的是服务器时间和本地时间的统一问题,否则可能导致合法的资源被当做过期处理,在项目中已采坑。。

如果响应头部中包含Cache-Control:max-ageCache-Control:s-max-age字段,则Expires字段会被忽略;

// 示例
Expires: Wed, 21 Oct 2015 07:28:00 GMT

缓存校验机制

如果由于服务器时间精度问题;或者如果设置的缓存时间已经过期,并且服务端资源并未发生更新,仅使用max-age等类似的缓存过期判断策略,会重新下载整个并未发生变化的资源,这会造成带宽浪费与服务器负载压力;

Last-Modified与If-Modified-Since
  1. Last-Modified:响应头部字段;表示服务器端资源上次修改的日期和时间,通常与请求字段If-Modified-Since搭配使用;其精度低于ETag,故常作为备用机制
    // 用法
    Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
    
    // 示例
    Last-Modified: Wed, 20 May 2019 11:11:11 GMT
    
    
  2. If-Modified-Since:请求字段(GET/HEAD方式),如果请求的资源在给定时间后未被修改,那么返回一个不带响应主体的304响应;否则是一个200的请求;同样,如果同时设置了If-None-MatchIf-Modified-Since也会被忽略;
    // 示例
    If-Modified-Since: Wed, 20 May 2019 11:11:11 GMT
    
ETag与If-None-Match

Last-Modified的精度与服务器的时间精度有关,无法识别一秒内进行多次修改的情况,也就是说资源的修改时间有可能存在一定偏差,而ETag可以精确标识资源的改动;另外,如果资源修改而其内容未发生变化(修改后再恢复)的情况下还是会重新加载资源;

  1. ETag相当于资源的摘要,值是一段ASCII码组成的字符串,仅在其内容发生变化时对应的ETag值才会改变;这样就可以避免Last-Modified中资源被修改而内容未发生变化的状况了;ETag的生成方式不唯一;

    服务器响应资源请求时,可以在响应头部加上ETag指令,如ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4",客户端可以选择是否缓存该资源及标识并且在下次请求该资源时在请求头中加入一个If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"的头部字段;服务器将当前版本的ETag与客户端的ETag字段值进行比较,如果两者保持一致则表示资源未被更改,返回304;

    需要注意的是,上边已经提到了ETag生成的方式不唯一,可以通过hash散列或者其他算法获得;因此在使用CDN或者其他分布式服务器系统时,需要保障ETag的唯一性,从而避免不必要的资源请求;当然,计算ETag对服务器性能也有一定的影响;

    W/为可选参数,用于标识是否为弱校验;ETag支持强校验和弱校验:强校验需要资源每个字节都对应相同,包括Content-Type、Content-Encoding等;弱校验仅需二者在语义上相同,即可以使用不同的Content-Type值等,尤其是在动态生成内容上弱校验更能发挥作用;

    // 示例
    ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    ETag: W/"0815"
    
F5与Ctrl+F5

Ctrl+F5会强制不用所有缓存并全部向服务器发送新的请求,请求头中移除了If-Modified-SinceIf-none-Match标头,并且加入Cache-Control: no-cachePragma: no-cache

F5则会加入请求头If-Modified-SinceCache-Control: max-age=0请求头以有效使用缓存;

文献5的图更形象地表明了客户端状态码与请求头部的关系:
HTTP缓存

结合上图,为了更高效地利用HTTP缓存,应当尽量为可缓存资源设置Expires/Cache-Control及ETag/Last-Modified标头,这样可以能减少304请求造成的性能开销,同时能在资源更新的时候尽可能早的获取新版本的资源,从而使用户体验更好;现在,使用webpack构建项目时,通常会将静态资源的文件名后缀加上md5等,这样可以在资源有新版本时直接更新资源,减少304的开销;


参考文献:

  1. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
  2. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ
  3. https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn
  4. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Pragma
  5. https://imweb.io/topic/5795dcb6fb312541492eda8c
  6. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Last-Modified
  7. https://zh.wikipedia.org/wiki/HTTP_ETag