面试老生常谈 - HTTP 缓存机制详解

9 阅读5分钟

一、缓存的两大类型

HTTP 缓存分为两种:强缓存协商缓存

类型是否请求服务器返回状态码控制字段
强缓存❌ 不请求200 (from cache)Cache-Control / Expires
协商缓存✅ 请求,但只发头部304 Not ModifiedETag / Last-Modified

二、强缓存

浏览器直接从本地读取资源,不发任何请求给服务器。

控制字段

Cache-Control(推荐,HTTP/1.1)

Cache-Control: max-age=86400
含义
max-age=N资源在 N 秒内有效,直接使用缓存
no-cache不使用强缓存,每次都走协商缓存验证
no-store完全不缓存,每次都重新请求
public可被代理服务器缓存
private只能被浏览器缓存,不允许代理缓存
immutable资源永远不会变,告知浏览器不要发条件请求

Expires(旧,HTTP/1.0)

Expires: Wed, 01 Jan 2026 00:00:00 GMT

用绝对时间控制过期,缺点是依赖本地时钟,与服务器时间可能不一致。Cache-Control 优先级高于 Expires

流程图

浏览器发起请求
     │
     ▼
本地有缓存? ──否──▶ 发请求到服务器
     │
     是
     │
     ▼
缓存未过期(max-age 内)?
     │                │
    是                否
     │                │
     ▼                ▼
直接使用缓存      走协商缓存流程
200 (from cache)

三、协商缓存

强缓存失效后,浏览器带着验证标识去问服务器:我这份缓存还有效吗?

  • 服务器说"还有效" → 返回 304 Not Modified,浏览器用本地缓存
  • 服务器说"变了" → 返回 200,携带新资源

方案一:Last-Modified / If-Modified-Since

第一次请求,服务器响应:

Last-Modified: Tue, 04 Mar 2025 10:00:00 GMT

后续请求,浏览器携带:

If-Modified-Since: Tue, 04 Mar 2025 10:00:00 GMT

服务器比较时间,未修改则返回 304。

缺陷:

  • 精度只到秒级,1秒内多次修改无法感知
  • 文件内容没变但 mtime 变了,也会被认为"已修改"(如重新部署、touch 操作)

方案二:ETag / If-None-Match(推荐)

ETag 是服务器根据文件内容生成的唯一标识符(类似内容指纹/哈希),只要内容不变,ETag 就不变。

第一次请求,服务器响应:

ETag: "abc123def456"
Cache-Control: no-cache

后续请求,浏览器携带:

If-None-Match: "abc123def456"

服务器逻辑:

If-None-Match 的值 == 当前资源 ETag?
       │                    │
      是                    否
       │                    │
       ▼                    ▼
  304 Not Modified      200 + 新资源 + 新 ETag
  (浏览器用本地缓存)

ETag 的两种类型

类型格式含义
强验证"abc123"字节级完全一致才匹配
弱验证W/"abc123"语义相同即可匹配(内容基本相同,允许微小差异)

ETag vs Last-Modified 对比

对比项Last-ModifiedETag
精度秒级内容级(哈希)
内容未变但时间变了❌ 误判为已修改✅ 正确命中缓存
内容变了但时间没变❌ 误判为未修改✅ 正确返回新资源
服务器性能开销略高(需计算哈希)
优先级(两者共存时优先用 ETag)

两者可以同时使用,优先使用 ETag 验证。


四、强制刷新

浏览器提供了几种方式绕过缓存,行为各不相同。

普通访问(正常导航)

  • 地址栏回车 / 点击链接
  • 走完整缓存流程:先强缓存,再协商缓存

普通刷新(F5 / Cmd+R)

  • 浏览器跳过强缓存,直接发请求
  • 请求头会带上 Cache-Control: max-age=0If-None-Match / If-Modified-Since
  • 服务器可能返回 304
请求头示例:
Cache-Control: max-age=0
If-None-Match: "abc123"
If-Modified-Since: ...

强制刷新(Ctrl+Shift+R / Cmd+Shift+R)

  • 浏览器完全绕过本地缓存
  • 请求头带上:
Cache-Control: no-cache
Pragma: no-cache
  • 不携带 If-None-Match 等验证头
  • 服务器必须返回完整资源(200)

三种刷新方式对比

方式强缓存协商缓存结果
普通访问✅ 使用过期后使用200 from cache / 304
F5 刷新❌ 跳过✅ 使用304 或 200
Ctrl+Shift+R❌ 跳过❌ 跳过必定 200

五、完整请求流程总结

浏览器发起请求
      │
      ▼
  强制刷新? ──是──▶ 直接请求服务器 ──▶ 200 返回完整资源
      │
      否
      │
      ▼
  本地有缓存 且 未过期(强缓存命中)?
      │                      │
      是                     否
      │                      │
      ▼                      ▼
200 (from cache)    携带 ETag/Last-Modified 请求服务器
                             │
                    服务器对比资源是否变化
                        │         │
                       否         是
                        │         │
                        ▼         ▼
                   304 Not     200 + 新资源
                   Modified
                  (用本地缓存)

六、实践建议

HTML 文件

Cache-Control: no-cache

HTML 是入口,需要每次验证,避免用户拿到旧版页面。

JS / CSS / 图片(带 hash 的文件名)

Cache-Control: max-age=31536000, immutable

app.a1b2c3d4.js,内容变化时文件名也变,所以可以永久缓存。

API 接口

Cache-Control: no-store

动态数据不应被缓存。


七、常见面试题速答

Q:ETag 和 Last-Modified 有什么区别?

ETag 基于内容哈希,精度更高;Last-Modified 基于时间,存在秒级误差和时间漂移问题。两者共存时优先使用 ETag。

Q:304 和 200 from cache 有什么区别?

200 from cache 是强缓存命中,完全不发网络请求;304 是协商缓存命中,发了请求但服务器告知"未修改",响应体为空。

Q:no-cache 和 no-store 有什么区别?

no-cache 不是"不缓存",而是"不使用强缓存,每次都要协商验证";no-store 才是真正的不存储任何缓存。

Q:为什么强制刷新后还是旧资源?

强制刷新只针对浏览器本地缓存,如果中间有 CDN 或代理服务器,CDN 层的缓存不会被清除,需要手动刷新 CDN。