一文搞懂HTTP缓存,面试再也不怕了

908 阅读9分钟

前言

当我们访问浏览器网址的时候,浏览器会加载各种资源(如HTML、CSS、JS、图片等)。如果网络环境不是很好,首次加载该网址的响应是相对较慢的,后续的访问却会快很多。这就是浏览器缓存带来的性能优化了。那么我们就去了解一下浏览器缓存(HTTP缓存)

一.缓存目的

a)减少网络请求次数,降低服务器压力,提高网页加载速度
b)提升网页加载速度,提高用户体验

二.缓存流程

客户端首次请求: 向服务器发起HTTP请求
服务端响应: 服务器收到请求后,返回资源数据,并根据缓存策略返回响应标头。通过Cache-Control指定缓存, Expiresmax-ageage控制缓存时间,ETag控制版本等
客户端再次请求相同资源: 检测是否有强缓存,有效直接使用缓存的副本
a) 强缓存失效则通过协商缓存: 使用ETagIf-None-MatchLast-ModifiedIf-Modified-Since判断是否有效
a1) 协商缓存-有效: 服务器返回304 复用缓存副本
a2) 协商缓存-无效: 服务器返回数据资源,缓存该资源的新副本

为了更直白详细的展示,我们给缓存的流程画一个流程图

缓存流程.png

三.缓存类型

如上缓存流程图所示, 缓存分为 强缓存协商缓存

强缓存(Strong Cache)

强缓存不会向服务器发送请求,直接从缓存中读取资源 主要通过设置HTTP响应头中的ExpiresCache-Control来控制
image.png

Cache-Control (HTTP/1.1 优先级高于Expires)

常用指令说明参数值
max-age=<seconds>缓存有效期(单位:秒)必填,eg: max-age=3600 表示缓存有效期为1小时
no-cache不使用强缓存,但响应头中会包含缓存标识,用于协商缓存 同Pragma(HTTP/1.0): no-cache-
no-store完全禁止缓存(强制进入重新请求阶段)-
public浏览器和代理服务器都可以缓存-
private仅允许浏览器缓存(默认)-
immutable永远不会改变可以加上该参数,谨慎使用-

Expires (HTTP/1.0遗留字段)

Expires: 指定资源过期时间 eg: Expires: Wed, 15 Apr 2026 03:19:25 GMT

问题: 判断缓存是否过期依赖本地时间,意味着可以通过修改客户端本地时间让缓存延期

协商缓存(Negotiate Cache)

强缓存失效时,浏览器携带验证信息向服务器确认资源是否变化

image.png

ETag(优先级高于Modified)/If-None-Match

ETag: 服务器返回资源的唯一标识符(哈希值)

参数: W/hash ETag: W/"6800ea08-18e8b" 或 hash ETag: "6800ea08-18e8b"

  • W/标识: 对大小写敏感,表示使用弱验证器
If-None-Match: 请求标头版本号

浏览器下次请求时携带该标识(浏览器在请求资源时附加的上次请求资源时服务器返回的ETag) 用于和服务器的ETag比较

Last-Modified/If-Modified-Since

Last-Modified(资源最后修改时间) 响应标头

服务器返回资源时附加最后修改时间 eg: Last-Modified: Wed, 09 Apr 2025 02:50:59 GMT

If-Modified-Since 请求标头

浏览器在请求资源时附加上次请求资源时服务器返回的最后修改时间 eg: If-Modified-Since: Wed, 09 Apr 2025 02:50:59 GMT

四.缓存存储位置分类

1.浏览器端存储

内存缓存(Memory Cache):临时存储高频资源,随标签页关闭释放

  • 优先存储 高频的小型资源(eg: JS/CSS), 速度快,但容量有限

磁盘缓存(Disk Cache)‌:持久化存储低频资源,重启后仍有效‌

  • 存储低频或较大资源(如图片),容量更大且持久化‌

Service Worker缓存‌:通过脚本独立管理缓存,需HTTPS协议支持

2.网络层

代理服务器/CDN缓存‌:由Cache-Control: public控制,允许中间节点缓存资源
HTTP/2推送缓存(Push Cache)‌:会话级临时存储,有效期约5min

缓存优先级: Service Worker 缓存 > 内存缓存 > 磁盘缓存 > HTTP/2 推送缓存(Push Cache)

五.缓存的分类

HTTP Caching 标准中,有两种不同类型的缓存:私有缓存共享缓存
私有缓存
私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应

共享缓存
共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存和托管缓存 有兴趣的可以更多参考

六.HTTP缓存流程

发起HTTP请求
 ↓
1. 是否被 Service Worker 拦截?
   ├─ 是 → 由 SW 策略决定返回缓存还是网络
   └─ 否 → 进入浏览器 HTTP 缓存处理
       ↓
2. 查找内存缓存(Memory Cache)
   ├─ 命中且未过期 → 直接使用(200 from memory cache)
   └─ 未命中 → 查找磁盘缓存(Disk Cache)
       ├─ 命中但需验证 → 跳到步骤 4(协商缓存)
       ├─ 命中且有效(强缓存) → 从磁盘读入内存并使用(200 from disk cache)
       └─ 未命中或无缓存 → 跳到步骤 3(网络请求)
3. 发起网络请求
   ↓
4. 如果需要验证缓存(或强缓存失效)
   ├─ 携带 `If-None-Match` / `If-Modified-Since` 请求服务器
   ├─ 服务器返回 304 → 使用本地缓存,更新时效
   └─ 服务器返回 200 → 接收新数据
5. 根据响应头决定是否缓存及如何缓存
   ├─ 存入磁盘缓存(持久化)
   └─ 同时存入内存缓存(供当前页使用)

关于响应头缓存控制逻辑

一、决定“能不能存”——no-store 与 no-cache

浏览器首先检查 Cache-Control 中是否包含:

  • no-store
    绝对禁止任何形式的缓存。资源不会被写入内存,也不会写入磁盘。每次请求都必须重新从服务器获取完整响应。
    典型场景:包含敏感数据的接口、实时性要求极高的数据。
  • no-cache
    可以缓存,但每次使用前必须向服务器验证。这是“协商缓存”的强制触发标志。
    资源仍会被存入磁盘和内存,但不会直接使用,必须先发验证请求(带 If-None-Match 或 If-Modified-Since),收到 304 后才使用缓存。

如果一个响应同时有 no-store 和 no-cacheno-store 优先级更高,直接不存。

二、决定“能强缓多久”——max-ageExpires、启发式

如果允许存储(没有 no-store),接下来浏览器计算该缓存可以在不询问服务器的情况下直接使用多久。

1. Cache-Control: max-age=<seconds>(优先级最高)
  • 指定从响应生成时刻(Date 头)起,资源可被视为“新鲜”的秒数。
  • 在这段时间内,浏览器直接从内存/磁盘拿缓存,连网都不发。
  • 例:max-age=3600 表示 1 小时内强缓存有效。
2. Expires: <HTTP-date>(HTTP/1.0 遗留,优先级低于 max-age
  • 一个绝对过期时间。如果超过了这个时间,缓存就“过期”了。
  • 如果同时存在 max-age,则忽略 Expires
3. 启发式缓存(Heuristic Caching)

既没有 max-age 也没有 Expires,但允许缓存时,浏览器会根据其他信息猜测一个有效期。

常见算法:

有效期 = (Date - Last-Modified) * 10%

即取文件最后修改时间到现在的时间差的 10%。 MDN 启发式缓存
这个机制就是 HTML 默认被强缓存的原因之一

三、决定“过期后怎么处理”——must-revalidate 等

缓存过期后,浏览器会发起协商验证。但也有一些指令可以改变这一行为:

  • must-revalidate
    只能在缓存未过期时直接用;一旦过期,必须向服务器验证,不能使用过期的缓存(比如在断网时也不能用)。
  • proxy-revalidate
    与 must-revalidate 相同,但只针对公共缓存(CDN 等),浏览器私有缓存可忽略。
  • stale-while-revalidate=<seconds>
    过期后,允许在指定秒数内先使用过期的缓存,同时后台异步请求新资源更新缓存。常用于提升体验,但不适用于强一致性数据。
  • stale-if-error=<seconds>
    过期后,如果服务器返回 5xx 或网络错误,允许再使用一段时间过期的缓存。

四、决定“谁可以缓存”——public / private

  • private
    表明响应只能被单个用户的浏览器缓存,不允许被中间代理(如 CDN、Nginx 反向代理)缓存。
    典型:包含用户个人信息的接口。
  • public
    表明响应可以被任何缓存节点存储(包括浏览器、CDN)。
    即使有 max-age,如果响应头包含 Authorization,某些浏览器可能默认为 private,显式加上 public 可以强打破这个限制。

五、协商缓存的响应头基石——ETag / Last-Modified

这些头本身不控制是否缓存或过期,而是为验证提供依据

  • ETag:资源的唯一指纹(通常为哈希)。浏览器下次会用 If-None-Match 带上这个值。
  • Last-Modified:资源最后修改时间。浏览器下次会用 If-Modified-Since 带上这个值。

当浏览器发起协商验证时,会同时带上两者(如果有)。服务器一次判断即可。

六、综合决策流程图(浏览器缓存控制总结)

收到响应后,浏览器执行以下逻辑(伪代码):

if (Cache-Control 包含 no-store) {
    不缓存,结束。
}

if (Cache-Control 包含 no-cache) {
    缓存资源,但立即标记为“必须验证”。
} else {
    计算新鲜度:
    if (Cache-Control 包含 max-age) {
        新鲜时间 = Date + max-age
    } else if (Expires 存在) {
        新鲜时间 = Expires
    } else {
        新鲜时间 = Date + (Date - Last-Modified) * 10%   // 启发式
    }
    在新鲜时间内,资源可直接使用。
    过期后,行为由 must-revalidate 等指令决定。
}

// 存储时:
如果需要缓存,将该响应体连同 ETag、Last-Modified、过期时间等一起存入磁盘(并可能载入内存)。