闲聊 HTTP 缓存
具体 HTTP 缓存标准就不细聊了,网上已经有很多相关总结的文章,随便聊聊最近一些时间实践中碰到的一些与缓存相关的问题或学习到的知识着重写一写吧
不同的域名访问相同的资源
曾经,有一种优化资源加载速度的方式,我们可以将资源地址替换为被广泛使用的分发 JS 包的 CDN(如 unpkg),这样,如果我们使用的这个资源恰好被其他网站也使用了,而恰好用户先访问过使用了这个资源的网站,那么再访问我们自己的网站的时候,这个资源就能命中缓存了
举个例子:
比如我们将引入 React 的方式改为使用 https://unpkg.com/react@16.7.0/umd/react.production.min.js
,如果 A 站点和 B 站点都使用了该资源,就可以共享缓存了
然而,这样的策略早已被扔进历史的垃圾桶里了,出于安全因素考虑,现在主流的浏览器的缓存策略还将访问资源的域名纳入缓存命中算法的考虑之中
CDN 缓存与跨域
我们经常会使用 CDN 来加速自己的静态资源,减轻源站压力。然而,引入一个中间层除了带来好处,也会引入复杂性。以阿里云 CDN 为例子,当我们的静态资源被缓存到阿里云 CDN 上,默认情况下,当时请求源站所返回的一些 HTTP 头部也会被后续缓存命中而返回
举个例子,假设 A 站点(a.example.com
)发起跨域请求访问了 cdn.zhuscat.com/conf
,源站返回了跨域头,并被 CDN 缓存,如:
然后 B 站点(b.example.com
)也发起同样的跨域请求,此时如果命中了 CDN 缓存,可能也会返回该头部,此时就会报跨域错误
解决这种问题的方式有很多种,也因你使用的 CDN 服务而异,比如阿里云 CDN 会识别源站返回的 Vary 响应头,比如针对上面所说的跨域情况,源站可以返回:
这样阿里云 CDN 还会将 Origin 字段纳入缓存命中算法之中,这样即使 A 站点和 B 站点访问的是同一份资源,但因为 Origin 不同,会分别回源
在阿里云 CDN 还提供了设置跨域头的功能,另外还有编写边缘脚本的能力,我们可以在边缘脚本中动态返回跨域头
总之,对于这种非透明的中间层,我们还是需要了解这一层的逻辑,才能比较迅速地排查和解决一些问题,不同的 CDN 服务商提供的功能可能有差异,但原理都是相通的,我们需要熟练掌握基础知识,培养解决一类问题的思路
启发式缓存
所谓启发式缓存,就是资源没有明确指定缓存策略,但我们通过各种先验的经验,设计一套策略,去缓存某些资源
比如当我们返回的资源,响应头中有 last-modified
,但是没有 expires
和 cache-control
,浏览器会根据 last-modified
结合当前时间,根据一定的算法去算出缓存时间,比如最近一次更改已经是很久之前了,就多缓存一会儿,这里的逻辑是,既然一个资源这么久没改了,那就猜测可能后续一段时间这个资源还是不会被更改。这里不纠结浏览器的具体算法细节,比如是否针对不同类型的资源是否会有不同的策略,计算时间的公式等等。毕竟,浏览器开发厂商是可能不断地去更改这个策略的,死记硬背没有用,当真的需要具体策略时候去看就行了。但是这种启发式缓存这样的行为指导我们的一件事情是,如果我们有明确控制缓存的需求,最好是给资源都明确地返回 cache-control
,否则可能出现我们更新了资源,用户访问到的还是老资源的情况
另外,这种启发式缓存不仅仅是在浏览器有,像阿里云 CDN 回源,如果源站返回的资源响应头也是上面说的那样的,也会有一个计算在 CDN 缓存时间的算法
如果 if-modified-since 和 if-none-match 都存在
这种情况下面,按照标准的实现,如果 if-none-match
存在,则以 if-none-match
更为优先,忽略 if-modified-since
了解你的资源是如何被访问的
我们对外提供的静态资源,不一定就是在主流的浏览器中被访问的。比如可能在 App 中可能会访问我们的资源,这个时候 App 是否能够遵循我们返回的 HTTP 响应头来进行缓存,App 中的缓存策略是怎样的,这也是我们需要弄清楚的
或者我们的站点主要用途就是内嵌到某个微信小程序的 webview 里面的,那么我们需要弄清楚,在微信小程序的 webview,是否是按照 HTTP 标准去实现缓存策略的
总之,我们需要从实际出发,了解我们的静态资源都是被哪些客户端访问的,这些客户端的缓存策略是怎样的,以及我们的资源是否都按照我们的预期被缓存或者不被缓存