概述

HTTP缓存的特点充分体现在B/SC/S体系架构中,例如:跟用户接触的各种客户端(浏览器/应用程序),还有代理服务器(正向/反向/透明)和CDN等中间缓存/代理缓存。也就是说,HTTP缓存的范围在[客户端 , 源服务器)。通过制定HTTP Header缓存策略来实现客户端缓存或代理缓存,以提高用户体验和减少带宽压力。


缓存策略

目前,在可缓存范围内默认自动缓存一些不常改变的资源。一般的判断途径有如下两点:

  • 请求方法:GET、HEAD
  • 返回的状态码:206返回的不完整的资源、301永久重定向的URL和404返回的空页面等

若使用其它不可缓存的请求方法去请求已缓存的URI,则此URI下的缓存会失效

客户端缓存

默认在cache-control中加入了private,表示只有客户端才能缓存,以下以浏览器为代表。缓存中存储着请求过的可缓存资源(包括响应头信息),其实也就是缓存着HTTP响应报文

#HTTP/1.1 优先级高
## 强制缓存 (单位秒)
cache-control: max-age=600
##校验缓存 (与private一样默认存在响应头中,虽然没有显示出来)
cache-control: no-cache
#----------------------------------------------------#
#HTTP/1.0 优先级低,兼容作用
##强制缓存
expires: Mon, 25 Sep 2020 23:39:34 GMT
##校验缓存
pragma: no-cache
#----------------------------------------------------#
#其它
##不缓存资源
cache-control: no-store
##缓存资源必须校验后才能使用(不论是否过期)
cache-control: must-revalidate
#----------------------------------------------------#
#实验阶段的首部
##不变的资源,即使刷新也不校验缓存
cache-control: immutable
##下面两个一般会结合着用
##在x秒内可以先暂时使用过期资源,异步进行校验更新
cache-control: stale-while-revalidate=x(秒)
##异步校验更新失败时,如果在x秒内的话,那么还将使用过期资源
cache-control: stale-if-error=x(秒)


强制缓存

  • 强缓存/强制缓存:在max-ageexpires时间内,浏览器不会重新发送请求到服务器。响应头date值通常需要转换为系统当前时区,这里应转为GMT+8北京时间。

强制缓存剩余时间 = ( 响应时间date + 允许缓存时间max-age ) - 请求时间(当前系统时间)

总结


响应头 优先级 来源 精确程度
cache-control: max-age=600 由客户端去计算 较准确(除非用户修改系统时间 😑)
expires: Mon, 25 Sep 2020 23:39:34 GMT 由服务器生成 存在时区和客户端等因素,误差无法控制

响应头 作用 其它
cache-control: no-store 不缓存资源 效果相当于cache-control: no-cache=Location(只能在响应头)
cache-control: no-cache 不缓存过期资源 请求头中的作用是直接向源服务器请求资源

疑问:当no-store和no-cache同时存在时,缓存策略会如何选择


响应头 作用 其它
cache-control: must-revalidate 缓存资源必须到源服务器校验后才能使用 跟proxy-revalidate的效果一样,只是这个作用范围是全局的(适用于客户端和缓存服务器)

校验缓存

  • 对比缓存/协商缓存/校验缓存:当强制缓存没有设置或已过期,就会检测缓存资源是否存在etaglast-modified(按优先级),然后携带对应的if-none-matchif-modified-since请求头到服务器中校验一致性。若一致,则返回304并更新缓存信息(date等),然后从缓存中获取相应资源;若不一致,则返回200并在body中携带该资源,同样也会更新缓存信息。

etag值在Nginx中默认通过计算last-modifiedcontent-length生成形如:etag: W/“5f6967ee-357e”。W表示Weak,弱etag的意思。

touch命令可以很容易地修改文件的最后修改时间,内容长度也可以在修改后保持不变。如果想要避免误差,尽量自定义生成强etag,可以采用GUID、MD5、sha1或者更强的散列值,如:sha256或sha512等。

总结


请求头/响应头 优先级 来源 精确程度
if-none-match/etag 默认生成16进制的组合值,消耗性能 较准确(因算法而异)
if-modified-since/last-modified 直接获取文件的last-modified值,比较容易 误差在1秒内

其它

除了前面提到的首部外,还有一些针对客户端的不常用的请求头,一些可能存在兼容性问题,了解即可。


请求头 作用
cache-control: max-stale=x(秒) 客户端可接受过期不超过x秒的资源
cache-control: min-fresh=x(秒) 要求缓存服务器中响应资源的age不超过x
cache-control: no-transform 跟下面的代理缓存一节中说的一样
cache-control: only-if-cached 客户端只接受已经缓存的资源响应

代理缓存

必须显式在cache-control中加入public,才能使中间缓存/代理缓存生效。实际上,客户端并不知道自己发送的请求是由缓存服务器响应的,还是由源服务器响应的。

源服务器针对缓存服务器的相关首部设置

#缓存服务器常见的响应指令,多个值用逗号隔开
cache-control: public, s-maxage=100, no-transform, proxy-revalidate
##缓存计时 (单位秒)
age: 9593360
##值的类型为<header-name>
vary: Origin, Accept-Encoding, User-Agent, Cookie

总结


响应头 作用
cache-control: public 允许使用代理缓存
cache-control: s-maxage=100 覆盖max-age和expires,只适用于缓存服务器
cache-control: proxy-revalidate 必须向源服务器重新验证资源有效性才能返回给客户端,只适用于缓存服务器
cache-control: no-transform 例如:又拍云图床CDN会将图片转成webp缓存以提升响应速度,此指令是不允许的
age: 9593360 资源在缓存服务器上停留的时间(缓存服务器系统当前时间 - 源服务器响应时间)
vary: User-Agent 根据header-name来缓存资源,这里表示根据客户端类型缓存资源(PC端请求返回PC端缓存;Browser端请求返回Browser端缓存)

当缓存服务器违反了资源响应头中的声明或出现其它意外状况时,就会给客户端返回warning警告码

存疑

public与private

在前面所提到的private与public的区别是网上普遍认同的,但在《图解HTTP》一书中谈到的private却有所不同,它表明private也可以缓存在缓存服务器上。只是这个缓存不是跟public一样可以无差别响应给所有到来的请求,而是响应给特定用户。这应该是根据Cookie来标识的吧,有条件的可以测试一下。

缓存位置

优先级从上至下依次降低

  • Service Worker:运行在浏览器背后的独立线程,仅支持HTTPS加密。可以由开发者自由控制缓存哪些文件、如何读取和匹配缓存等,且缓存可持续有效。到flokk页面中体验一下吧。
  • memory cache:关闭标签页后失效(临时存储位置,响应迅速,忽略强制缓存和校验缓存的束缚)
  • disk cache:保存在硬盘长久有效,除非用户主动清理(强制缓存和校验缓存的主要存储位置)
  • Push Cache:HTTP2中的推送缓存,只在Session会话期间有效,时效极短。虽然可以推送任何资源,但只能使用一次。功能上的支持依赖于浏览器,详情参考HTTP/2 push

缓存的目的是提高用户访问速度和减少带宽占用,所以真正的物理缓存位置目前只有memory cache和disk cache两种,而Service Worker和Push Cache只是一种控制缓存的技术,它们不受HTTP Header缓存策略的影响。Service Worker通过请求拦截技术,根据自定义的逻辑和优先级从memory cache或disk cache中取出资源,但请求中显示的还是from Service Worker。Push Cache是HTTP2的技术,浏览器有针对的缓存策略。

一个有趣的地方是,CSS资源总是优先存储在disk cache中,因为它只需要在页面渲染时加载一次;而JS、字体和图片等需要频繁解析或加载,所以优先存储在memory cache

用户在浏览器上的行为

常见操作

  1. 键入网址后Enter(直接访问):按上面的优先级获取缓存数据(根据memory cache的时效性,一般是没有从那里返回的数据,除非用户将未关闭的标签页网址放到新建标签页中再次访问),若没有则向服务器发送请求。
  2. Ctrl + R/F5(普通刷新):按上面的优先级获取缓存数据,若没有则向服务器发送请求
  3. Ctrl + Shift + R/Ctrl + F5(强制刷新):直接向服务器发送请求

三种行为的异同

不同点


  • 直接访问会先后触发强制缓存校验缓存
  • 普通刷新会触发校验缓存,请求会带上cache-control: max-age=0(还有if-none-match和if-modified-since)
  • 强制刷新会忽略缓存,请求会带上cache-control: no-cache(不会带上if-none-match和if-modified-since),为了兼容还会带上pragma: no-cache。如果在控制台上勾选Disable cache,那么也相当于强制刷新的效果。


相同点


这三种操作只要没有触发强制缓存,那么都会更新所请求资源的缓存信息,也就是相关的头信息,如:date/expires/etag/last-modified等。不过,通过直接访问的方式很难不触发强制缓存,除非刚好过期或者开发者没设置 😑



其它

  • 前进和后退:同样是按上面的优先级获取缓存数据,唯一不同的是,按下前进或后退按钮还会从标签页会话历史中恢复该页面最后记录的状态(该状态包括当时的响应头信息和滚动条位置等),称为标签页会话状态。新版浏览器还加入了Back-forward cache的功能,能从内存中恢复当时的完整状态(包括JavaScript和DOM)。该功能在Chrome中还处于实验阶段,默认未启用,详情打开chrome://flags/搜索。
  • Ctrl + Shift + T:恢复上一次关闭的标签页和会话历史,其余效果跟直接访问类似。
  • Ctrl + Shift + delete:清空缓存


Nginx配置

在新版Nginx中,当expires被设置后,会根据当前系统时间计算并生成Cache-Control相应的max-age信息,以排除因时区差异所造成的影响。还有就是,Nginx默认会生成etaglast-modified响应头信息,也就是默认存在校验缓存,我只需要决定是否启用强制缓存即可 😋

server {

        #优先级从里到外,若location里面设置了add_header,则会忽略外面的设置
        location / {
                index index.html index.htm;
                expires 7d;
        }

        location ~\.html$ {
                try_files $uri =404;
                expires 7d;
        }

        location ~*\.(gif|svg|jpg|jpeg|png|bmp|ico|svg)$ {
                expires 365d;
        }

        location ~*\.(js|css) {
                expires 7d;
        }

        location ~*\.(mp3|ttf|woff|woff2) {
                expires 365d;
        }

}

上述设置因人而异,图片/字体等静态资源不会频繁更新,我将其设置成强制缓存1年;html/js/css等资源会被我频繁改动,所以强制缓存7天就好了。

资源如何更新

问题

当我们访问一个网站时,需要向服务器发送大量资源请求,而对于这些请求,浏览器是如何得知的呢?那是因为浏览器会先将要访问的网页资源加载进来,根据网页中嵌入的链接地址再去分别请求,如:JS、CSS、字体和图片等,此HTML网页相当于访问入口。

问:网页校验缓存返回200之后,其中的链接会如何获取资源?

如果链接地址没有改变,则根据HTTP Header缓存策略执行即可;若已改变,则重新发起请求呗~。但存在一种情况,例如:https://www.livejq.top/js/myfile.js资源链接。虽然我修改了这个资源,但文件名/链接没有变化,那么浏览器当然是不会主动重新去请求的,这种情况是非常糟糕的,会造成排版混乱甚至是内容不一致。在浏览器上,“不一般的用户”还可以执行手动强制刷新或清空缓存等操作,而在一些手机客户端上往往操作有限。

解决办法

给文件名添加版本号:https://weatherwidget.io/w/js/angular-1.5.8.min.js
给文件名添加编号(散列值或更新日期等):https://ssl.gstatic.com/accounts/o/1209952130-idpiframe.js
给文件添加版本路径:https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js

注意:现已不推荐使用类似myfile.js?v=1.2的QueryString方式,这会阻止某些代理服务器去缓存它


以上只是个人理解,难免错漏,仅供参考~

参考资料

  1. MDN HTTP 缓存
  2. Cache-Control
  3. 可缓存条件
  4. 不要使用QueryString
  5. 强制缓存与校验缓存
  6. 理解浏览器缓存机制
  7. etag如何生成
  8. 深入理解浏览器的缓存机制
  9. 在Chrome和Edge中使用Back Forward Cache
  10. 图解HTTP

留言评论
推荐阅读
  • Hexo博客的优化问题

    起因刚搭好博客后觉得还不错,很好,很精致。但久了之后就发现其中还存在的问题(访问加载速度太慢了),而这还是通过别人的提醒后才真正着手去...

    Hexo博客的优化问题
  • HTTP报文

    前言在TCP/IP四层模型之下,我们的计算机按部就班地执行着自己的任务。在网上冲浪的过程中,客户端在我们看不见的地方默默地发送着报文,...

    HTTP报文
  • 深入理解Java运算符

    优先级 As the Java programmer’s beginning 优先级从上到下递减,不要问为什么,因为这就像在问1...

    深入理解Java运算符