启发式缓存:无Cache-Control、Expires下浏览器默认强缓存

105 阅读4分钟

网络太久没看都忘了,记录一下

下面是客户的一个js资源的返回头信息,它的缓存策略是什么样的:

content-encoding: gzip
content-type: application/javascript
date: Tue, 18 Feb 2025 07:03:34 GMT
etag: W/"67b42142-1a80"
last-modified: Tue, 18 Feb 2025 05:57:22 GMT

第一次请求完资源之后,立马刷新浏览器,会显示from disk cache返回信息如下:

img_2025_02_18_15_34_22.png

在我隔了一段时间刷新之后返回信息如下(当然没有开启disable cache):

img_2025_02_18_15_32_30.png

是因为浏览器会在没有设置明显的缓存策略的时候,会有一个默认的启发式缓存策略

下面是详细内容,以及最终是否解决和如何解决这个问题

启发式缓存

下面来解释一下为什么会在连续刷新两次时会看到 "from disk cache" 的现象

这个现象看起来有点反直觉,因为在响应头时只看到了协商缓存的标记(ETag 和 Last-Modified),却没有看到强缓存的header,但这实际上涉及到了浏览器的一个默认行为机制:

  1. 浏览器默认行为:

    • 即使服务器没有明确指定 Cache-ControlExpires,浏览器也会有一个默认的启发式缓存策略
    • 当响应中包含 Last-Modified 时,浏览器会自动计算一个缓存时间
    • 通常这个时间是 (Date - Last-Modified) 的 10%
  2. 在上面的案例中:

    Date: Tue, 18 Feb 2025 07:03:34 GMT
    Last-Modified: Tue, 18 Feb 2025 05:57:22 GMT
    

    时间差约为 Date - Last-Modified = 1小时6分钟 因此浏览器可能会自动设置大约 3972秒(1小时6分) * 10% = 397秒(约6.6分钟)的本地缓存时间,这个时间内浏览器会直接使用本地缓存

  3. 连续刷新的表现:

    • 第一次访问:从服务器获取资源
    • 连续刷新:在这个短暂的默认缓存期内,浏览器会直接使用磁盘缓存(from disk cache)
    • 这就解释了为什么你看到 "from disk cache" 的提示

当前的缓存行为总结:

  • 实际上是一个"混合模式":
    • 短期内(浏览器启发式计算的时间内)表现为强缓存
    • 超过这个时间后变为协商缓存

可能会导致什么问题

可能导致资源更新不及时

如果服务器端的资源更新了,但是因为你的资源上回编辑的时间可能非常的久,比如1年前,那么浏览器的启发式缓存时间最长会是30天左右,那么在这个时间内,浏览器会一直使用本地缓存,导致资源更新不及时,用户想要访问新的资源需要手动的清除缓存或者等待缓存过期

造成不好的用户体验

如何解决

在mdn中启发式缓存的解释中,有一句话:

启发式缓存是在 Cache-Control 被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头

那么,解决这个问题的方法就是找后端在服务器端返回资源的响应头里明确指定缓存策略,比如:

// 强缓存 2h
Cache-Control: max-age=7200

// 禁用浏览器强缓存,使用协商缓存
Cache-Control: no-cache

这样就可以避免浏览器的默认启发式缓存策略,确保缓存策略的可控性,简单好用

有什么其他的方式解决这个问题吗?欢迎留言


2025年02月19日 星期三

看见一个文章前端如何优雅通知用户刷新页面?

那么可以对常修改的资源设置一个动态刷新的方式来更新资源:

点击展开 方法1:添加时间戳或随机数作为URL参数
function loadScriptWithTimestamp(url) {
   const timestamp = new Date().getTime()
   const src = url + (url.includes('?') ? '&' : '?') + `timestamp=${timestamp}`
   return new Promise((resolve, reject) => {
       const script = document.createElement('script')
       script.src = src
       script.onload = resolve
       script.onerror = reject
       document.head.appendChild(script)
   })
}
loadScriptWithTimestamp('/abc.js')
   .then(() => console.log('adc.js 加载成功'))
   .catch(error => console.error('加载失败:', error));

方法2:定时轮询检查资源是否更新

参考