为了提升页面加载性能,改善用户体验,我们希望我们的静态资源能够长期缓存在用户的浏览器中,为此,我们通常会给静态资源设置一个较长的 max-age ,而在资源内容有更新时,则生成一个名字不同的新文件,以便用户能够及时收到最新的内容。

我们都知道, HTTP 缓存可以分为强缓存和协商缓存,根据我们的配置,强缓存过期后,浏览器会向服务器发出一个校验请求,如果服务器判断资源没有更新过,则通知浏览器使用缓存,也就是我们常说的 304 响应。

Immutable

但是在现代前端工程体系下,一个静态资源从发布之日起,可能永远也没有更新的需求了(更新后文件名/版本号/hash值等参数一定会变,也就是相当于一个全新的资源了)。但是即使我们配置了较长的 max-age ,用户在刷新页面的时候,浏览器依然会向服务器发出请求,在用户体量较大的时候,对网络资源也有着极大的浪费。按照 Facebook 的统计,大约有 20% 的请求是响应了 304 的。为此, Facebook 建议给 Cache-Control 增加一个新的属性: dont-revalidate ,在响应头里加入该字段表示该资源永不过期,不需要再发送条件请求。这个字段最终被 Firefox 49 根据 Cache Control Extensions 规范实现为一个 Behavioral extensionimmutable 。我们可以这样使用:

1
Cache-Control: max-age=31536000, immutable

对于支持 immutable 的浏览器,会一直使用本地的缓存,而对于尚未支持的浏览器,也可以根据 max-age 平滑的降级。

浏览器支持情况

  • Firefox: 49+ (仅支持HTTPS请求)
  • Microsoft Edge: 15+
  • Safari: 24+
  • Chrome: 不支持,Chrome 自己实现了另一个解决这个问题的方案

Chrome 中的刷新

在看 Chrome 的新方案之前,我们先来看一下浏览器在刷新时做了什么:

F5

F5
F5

Ctrl + F5

Ctrl + F5
Ctrl + F5

可以看到,在按下 F5 时,Chrome 为请求加上了 max-age=0 的 Cache-Control,从而保证至少要向服务器进行一次过期校验。而在 Ctrl + F5 时,则更为激进的直接设置了 no-cache

这里有一张较详尽的表格可以参考:

各浏览器刷新表现
各浏览器刷新表现

好像和 immutable 关系不大?我们再来看看 Chrome 中静态资源的刷新:

静态资源刷新

为了方便测试,我们给资源设置了一个 8 秒的 max-age,等待 3 秒后,我们按下 F5:

max-age过期前刷新
max-age过期前刷新

好像有哪里不对?说好的 max-age=0 呢?说好的 304 呢?

一番操作之后我们找到了这里:
https://bugs.chromium.org/p/chromium/issues/detail?id=654378
https://bugs.chromium.org/p/chromium/issues/detail?id=611416

可以看到,在 Chrome 54+ 版本中,Chrome 开始在刷新页面时只为 main resource 也就是我们的 html 页面加上 max-age=0 的设置,其他资源会遵循资源自身设定的缓存策略。通过修改 reload 的行为, Chrome 同样大量降低了刷新页面带来的带宽消耗。

还是上面的页面,我们等待 10 秒之后再刷新一次验证一下:

max-age过期后刷新
max-age过期后刷新

可以看到, max-age=8 的静态资源响应变成了 304 ,符合我们的预期。

总结

由于我们的静态资源更新依赖于 html 中资源地址的更新,通常我们会对 html 文件设置为不缓存或仅缓存极短的时间。而绝大部分静态资源本身自发布起就不再需要更新了,所以我们可以大胆的将静态资源的缓存时间设置的足够长(比如一年),同时加入 immutable 属性,这样,几乎所有的现代浏览器都会帮我们取消缓存协商的过程,从而节约大量协商请求的带宽和流量。