跳到主要内容
预计阅读 36 分钟

浏览器篇 | 强缓存与协商缓存

前言

HTTP 缓存大概是前端面试中”最容易答对一半”的话题。Cache-ControlETag304 这些关键词你可能都知道,也能大概说出”强缓存不发请求、协商缓存要发请求”。但如果面试官追问下去:

  • ExpiresCache-Control 的区别?为什么要用 Cache-Control 替代 Expires
  • no-cacheno-store 的区别是什么?no-cache 到底是”不缓存”还是”缓存但要验证”?
  • ETagLast-Modified 同时存在时,谁的优先级更高?
  • 实际项目中,HTML 文件和静态资源(JS/CSS/图片)的缓存策略应该怎么设计?
  • Service Worker 的缓存和 HTTP 缓存是什么关系?

很多人就开始犹豫了。

本章我们就把 HTTP 缓存的完整流程——从浏览器发起请求到拿到响应的每一步判断——讲得清清楚楚。


诊断自测

Q1:Cache-Control: no-cache 的含义是什么?它和 no-store 有什么区别?

点击查看答案

no-cache 的含义不是”不缓存”,而是”缓存,但每次使用前必须向服务器验证”(协商缓存)。浏览器会保存响应的副本,但下次请求时一定会发请求给服务器,由服务器判断缓存是否过期。

no-store 才是”完全不缓存”——浏览器不保存任何响应副本,每次都要完整请求。

Q2:下面的响应头会导致什么缓存行为?

Cache-Control: max-age=31536000
ETag: "abc123"
点击查看答案

浏览器会将响应缓存 31536000 秒(365 天)。在这段时间内,再次请求同一资源时,浏览器直接使用缓存,不发请求(强缓存命中,状态码 200 from cache)。365 天后缓存过期,浏览器会发请求并带上 If-None-Match: "abc123" 头,由服务器判断资源是否变化(协商缓存)。如果没变,返回 304;如果变了,返回 200 + 新资源。

Q3:为什么 HTML 文件通常不设置长时间强缓存,而 JS/CSS 文件可以?

点击查看答案

因为 HTML 文件是入口文件,它引用了 JS/CSS 等资源的 URL。如果 HTML 被强缓存了,即使你更新了 JS/CSS 文件并改了文件名(加了 hash),用户拿到的还是旧的 HTML,引用的还是旧的 JS/CSS URL。

而 JS/CSS 文件通常通过文件名带 hash(如 app.a1b2c3.js)来做版本管理。内容变了,hash 变了,URL 变了,浏览器就会当作新资源请求。所以可以放心给它们设置超长的强缓存。


一、HTTP 缓存的整体流程

当浏览器要请求一个资源时,会经过以下判断流程:

浏览器发起请求


是否有缓存副本? ── 否 ──► 向服务器发起请求 → 返回 200 + 资源
    │ 是

强缓存是否过期? ── 否 ──► 直接使用缓存(200 from cache)
    │ 是

发请求给服务器(带上验证信息)


服务器判断资源是否变化?
    │                  │
    否                 是
    ▼                  ▼
返回 304           返回 200 + 新资源
(使用缓存副本)   (更新缓存)

整个流程分为两个阶段:

  1. 强缓存:浏览器自己判断缓存是否过期,不发请求
  2. 协商缓存:强缓存过期后,向服务器验证缓存是否仍然有效

二、强缓存

强缓存的特点是:不需要向服务器发请求。如果缓存命中,浏览器直接从缓存中读取资源,状态码显示 200,Size 列显示 from disk cachefrom memory cache

2.1 Expires(HTTP/1.0)

Expires: Wed, 09 Mar 2027 08:00:00 GMT

Expires 指定了一个绝对时间,在这个时间之前,缓存有效。

问题:

  • 依赖客户端时间。如果用户手动修改了系统时间,缓存判断就会出错
  • 绝对时间格式容易出错
  • 已经是 HTTP/1.0 时代的产物,现代项目基本不单独使用

2.2 Cache-Control(HTTP/1.1)

Cache-Control: max-age=3600

Cache-Control 使用相对时间(以秒为单位),从收到响应的那一刻开始计时。上面的例子表示”缓存有效期 3600 秒(1 小时)”。

ExpiresCache-Control 同时存在时,Cache-Control 优先级更高。

Cache-Control 常用指令

# 缓存 1 小时
Cache-Control: max-age=3600

# 缓存 1 年(通常用于带 hash 的静态资源)
Cache-Control: max-age=31536000, immutable

# 缓存但每次必须验证(协商缓存)
Cache-Control: no-cache

# 完全不缓存
Cache-Control: no-store

# 只允许浏览器缓存,不允许 CDN 等中间代理缓存
Cache-Control: private, max-age=3600

# 允许所有中间节点缓存
Cache-Control: public, max-age=3600

最容易混淆的三个指令

指令含义是否缓存是否验证
max-age=3600缓存 1 小时,期间直接使用过期后验证
no-cache缓存,但每次使用前必须向服务器验证每次验证
no-store不缓存,每次都完整请求不适用

no-cache 的名字是最大的误导——它不是”不缓存”,而是”缓存但要验证”。真正的”不缓存”是 no-store

immutable

Cache-Control: max-age=31536000, immutable

immutable 告诉浏览器:在 max-age 期间,即使用户手动刷新页面,也不发请求验证

没有 immutable 时,用户按 F5 刷新页面,浏览器虽然有强缓存的资源,但仍可能发一个带 If-None-Match 的请求去验证。加了 immutable 后,刷新也直接用缓存。这对带 hash 的静态资源非常有意义——文件名里有内容 hash,内容不变 hash 不变,完全不需要验证。

from memory cache vs from disk cache

浏览器把缓存分为两个位置:

  • Memory Cache(内存缓存):读取速度极快,但容量有限。关闭标签页后清除
  • Disk Cache(磁盘缓存):读取速度相对慢,但容量大。关闭浏览器后仍然保留

浏览器会根据资源大小、使用频率等因素决定放在哪里。一般来说:

  • 小文件、频繁访问的资源(JS/CSS)→ Memory Cache
  • 大文件、不那么频繁的资源(图片)→ Disk Cache

三、协商缓存

当强缓存过期后,浏览器需要向服务器验证缓存是否仍然有效。这就是协商缓存。

协商缓存的特点是:一定会发请求到服务器,但如果资源没有变化,服务器只返回 304 Not Modified,不传输资源体,仍然使用本地缓存。

协商缓存有两组 header 机制:

3.1 Last-Modified / If-Modified-Since

第一次请求:

# 服务器响应
HTTP/1.1 200 OK
Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT

服务器告诉浏览器:这个资源最后修改时间是 2025-01-01。

后续请求(缓存过期后):

# 浏览器请求
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT

浏览器带上之前收到的 Last-Modified 值。服务器比较文件的实际修改时间:

  • 如果没变:返回 304 Not Modified,无响应体
  • 如果变了:返回 200 OK + 新资源

Last-Modified 的局限性:

  1. 精度只到秒:如果文件在 1 秒内被修改多次,Last-Modified 无法感知
  2. 内容没变但修改时间变了:比如 touch 一个文件,修改时间会更新,但内容没变。服务器会认为资源变了,返回完整响应,浪费带宽
  3. 分布式服务器时间可能不同步:不同服务器对同一文件的修改时间可能不一致

3.2 ETag / If-None-Match

为了解决 Last-Modified 的局限性,HTTP/1.1 引入了 ETag

第一次请求:

# 服务器响应
HTTP/1.1 200 OK
ETag: "a1b2c3d4"

ETag 是服务器为资源生成的唯一标识符,通常基于资源内容的 hash。内容不变,ETag 不变;内容变了,ETag 就变。

后续请求(缓存过期后):

# 浏览器请求
GET /style.css HTTP/1.1
If-None-Match: "a1b2c3d4"

服务器比较当前资源的 ETag 和请求中的 If-None-Match

  • 如果一致:返回 304 Not Modified
  • 如果不一致:返回 200 OK + 新资源

ETag 的类型

# 强 ETag:资源内容完全一致才匹配
ETag: "abc123"

# 弱 ETag:资源在语义上等价即可匹配
ETag: W/"abc123"

强 ETag 对内容做精确匹配(逐字节一致),弱 ETag 只要求”功能等价”(比如 HTML 中只是加了一个空格,功能没变)。

ETag vs Last-Modified

特性Last-ModifiedETag
精度内容级别
判断依据文件修改时间文件内容 hash
性能比较时间,开销小生成 hash 有一定开销
分布式一致性可能不一致内容一致则 hash 一致
优先级

ETagLast-Modified 同时存在时,服务器会优先判断 ETag 只有 ETag 匹配成功后,才会继续检查 Last-Modified(如果需要的话)。实际上,大多数服务器实现中,ETag 一致就直接返回 304 了。


四、完整流程图

把强缓存和协商缓存串起来,完整的 HTTP 缓存流程是这样的:

浏览器请求资源


本地有缓存? ── 否 ──► 请求服务器 → 200 + 资源 → 缓存
    │ 是

检查 Cache-Control / Expires


强缓存过期? ── 否 ──► 200 (from cache),不发请求
    │ 是

发请求到服务器(携带 If-None-Match / If-Modified-Since)


服务器判断 ETag / Last-Modified
    │                    │
    匹配                  不匹配
    ▼                    ▼
304 Not Modified      200 + 新资源
(使用本地缓存)       (更新缓存)

状态码含义

状态码含义缓存类型
200 (from memory/disk cache)强缓存命中,没发请求强缓存
304 Not Modified协商缓存命中,发了请求但服务器说”没变”协商缓存
200 OK缓存未命中或没有缓存,返回新资源无缓存

五、实战:缓存策略设计

理解了缓存机制后,最重要的是知道怎么在实际项目中配置。这也是面试官最喜欢问的实战问题。

5.1 HTML 文件:不缓存或协商缓存

# 方案一:完全不缓存
Cache-Control: no-store

# 方案二:协商缓存(推荐)
Cache-Control: no-cache
ETag: "html-hash-xxx"

HTML 是入口文件,必须保证用户拿到的是最新版本。 如果 HTML 被强缓存了,即使你发布了新版本的 JS/CSS,用户拿到的 HTML 仍然引用旧版本的资源。

推荐用 no-cache(协商缓存)而不是 no-store(完全不缓存),因为协商缓存至少在内容没变时能返回 304,省去传输 HTML 的时间。

5.2 带 hash 的静态资源:超长强缓存

Cache-Control: max-age=31536000, immutable

JS、CSS、图片等静态资源,通过构建工具(Webpack、Vite)在文件名中加入内容 hash

app.a1b2c3d4.js
styles.e5f6g7h8.css
logo.i9j0k1l2.png

文件内容变了 → hash 变了 → URL 变了 → 浏览器当作新资源请求。 文件内容没变 → hash 没变 → URL 没变 → 浏览器命中强缓存。

这就是所谓的基于内容寻址的缓存策略——用内容决定 URL,用 URL 决定缓存。

5.3 不带 hash 的静态资源:协商缓存

如果某些资源无法加 hash(比如第三方库的 CDN URL),可以用协商缓存:

Cache-Control: max-age=0
ETag: "resource-hash"

或者用较短的强缓存 + 协商缓存组合:

Cache-Control: max-age=600
ETag: "resource-hash"

10 分钟内走强缓存,过期后走协商缓存。

5.4 API 请求:通常不缓存

Cache-Control: no-store

接口数据通常是动态的,不应该被缓存。但某些不常变化的 API(如配置信息、字典数据)可以适当缓存:

Cache-Control: max-age=60

5.5 总结

资源类型缓存策略原因
HTMLno-cache + ETag入口文件,必须保证最新
JS/CSS(带 hash)max-age=31536000, immutable内容不变 URL 不变,长期缓存
图片/字体(带 hash)max-age=31536000同上
第三方库(无 hash)max-age=600 + ETag短期强缓存 + 协商缓存
API 数据no-store动态数据,不缓存

六、Service Worker 缓存

除了 HTTP 缓存,现代浏览器还提供了 Service Worker 缓存机制。它运行在浏览器后台的独立线程中,可以拦截网络请求并自定义缓存策略。

Service Worker 和 HTTP 缓存的关系

浏览器发起请求


Service Worker 拦截 ── 命中 SW 缓存 ──► 返回缓存的响应
    │ 未命中

HTTP 缓存(强缓存/协商缓存)
    │ 未命中

网络请求

Service Worker 的优先级高于 HTTP 缓存。 请求先经过 Service Worker,Service Worker 可以决定是走缓存、走网络、还是两者结合。

常见缓存策略

// 1. Cache First(缓存优先):适合不常变化的资源
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request);
    })
  );
});

// 2. Network First(网络优先):适合需要最新数据的场景
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request).catch(() => {
      return caches.match(event.request);
    })
  );
});

// 3. Stale While Revalidate:先返回缓存,后台更新
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      const fetchPromise = fetch(event.request).then((response) => {
        // 更新缓存
        caches.open('v1').then((cache) => {
          cache.put(event.request, response.clone());
        });
        return response;
      });
      return cached || fetchPromise;
    })
  );
});

Service Worker vs HTTP 缓存

特性HTTP 缓存Service Worker 缓存
控制粒度响应头控制JS 代码完全控制
离线支持不支持支持
灵活性有限(几个指令)完全自定义
优先级
适用场景常规缓存PWA、离线应用、复杂缓存策略

常见误区

误区一:“no-cache 就是不缓存”

no-cache 的意思是”缓存,但每次使用前必须向服务器验证”。真正不缓存的是 no-store。这可能是 HTTP 缓存中最广为人知的误区了。

误区二:“304 响应不走网络,和强缓存一样快”

304 响应虽然不传输资源体,但仍然需要一次完整的 HTTP 请求-响应往返(网络延迟 + 服务器处理时间)。而强缓存完全不发请求。在网络延迟高的情况下,304 和 200 的速度差异可能不如想象中大。

误区三:“ETag 就是文件的 MD5 值”

ETag 的值由服务器决定,没有固定的生成规则。有些服务器用文件内容的 hash,有些用”文件大小 + 修改时间”的组合,有些用自定义算法。规范只要求 ETag 在资源不变时不变、资源变了就变。Nginx 默认的 ETag 就是用”修改时间 + 文件大小”生成的,并不是内容 hash。

误区四:“给所有资源都设置长时间强缓存就行了”

不行。对于入口文件(HTML),长时间强缓存会导致用户无法获取最新版本。正确的做法是:HTML 用 no-cache(每次验证),带 hash 的静态资源用长时间强缓存。两者搭配才是完整的缓存策略。


小结

本章系统梳理了 HTTP 缓存的完整机制,从强缓存到协商缓存,从理论到实战。

核心要点

  1. 缓存流程:先检查强缓存(不发请求),过期后走协商缓存(发请求验证)
  2. 强缓存Cache-Control: max-age 设置相对过期时间,优先级高于 Expires
  3. no-cache ≠ 不缓存no-cache 是”缓存但每次验证”,no-store 才是”不缓存”
  4. 协商缓存ETag / If-None-Match 基于内容判断,优先级高于 Last-Modified / If-Modified-Since
  5. 实战策略:HTML 用 no-cache,带 hash 的静态资源用 max-age=31536000, immutable
  6. Service Worker 缓存优先级高于 HTTP 缓存,适合 PWA 和复杂缓存场景

本章思维导图

浏览器:HTTP 缓存
  • 整体流程
    • 有缓存?→ 强缓存过期?→ 协商缓存 → 200/304
  • 强缓存(不发请求)
    • Expires:绝对时间(HTTP/1.0,已过时)
    • Cache-Control:相对时间(HTTP/1.1,优先)
      • max-age:缓存秒数
      • no-cache:缓存但每次验证
      • no-store:完全不缓存
      • immutable:刷新也不验证
      • public / private
    • from memory cache vs from disk cache
  • 协商缓存(发请求验证)
    • Last-Modified / If-Modified-Since
      • 精度秒级,可能误判
    • ETag / If-None-Match
      • 内容级精度,优先级更高
    • 304 Not Modified
  • 实战策略
    • HTML → no-cache + ETag
    • 带 hash 静态资源 → max-age=31536000, immutable
    • API 数据 → no-store
  • Service Worker 缓存
    • 优先级高于 HTTP 缓存
    • Cache First / Network First / Stale While Revalidate
    • 支持离线访问

练习挑战

第一题(⭐ 基础):判断缓存行为

以下响应头,浏览器第二次请求同一资源时会怎样?

HTTP/1.1 200 OK
Cache-Control: max-age=0
ETag: "v1"
Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT
点击查看答案与解析

max-age=0 意味着强缓存立即过期,浏览器每次请求都会走协商缓存

第二次请求时,浏览器会发送:

GET /resource HTTP/1.1
If-None-Match: "v1"
If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT

同时携带 If-None-Match(对应 ETag)和 If-Modified-Since(对应 Last-Modified)。服务器优先检查 ETag

  • 如果 ETag 仍然是 "v1":返回 304,使用缓存
  • 如果 ETag 变了:返回 200 + 新资源

效果类似 no-cache:每次都验证,但内容没变时只传 304 响应头,不传资源体。

第二题(⭐⭐ 进阶):设计缓存策略

你的项目使用 Vite 构建,产出如下文件:

dist/
  index.html
  assets/
    app-a1b2c3.js
    vendor-d4e5f6.js
    style-g7h8i9.css
    logo.png           (注意:没有 hash)

请为每种文件设计合适的缓存策略(写出 Nginx 或 CDN 的配置逻辑)。

点击查看答案与解析
# index.html:协商缓存,保证用户拿到最新 HTML
location = /index.html {
    add_header Cache-Control "no-cache";
    # ETag 由 Nginx 自动生成
}

# 带 hash 的静态资源:超长强缓存
location /assets/ {
    # 匹配带 hash 的文件(app-a1b2c3.js 等)
    if ($uri ~* "\.[a-f0-9]{6,}\.(js|css)$") {
        add_header Cache-Control "max-age=31536000, immutable";
    }
}

# 不带 hash 的资源(logo.png):短强缓存 + 协商缓存
location ~* \.(png|jpg|gif|ico)$ {
    add_header Cache-Control "max-age=86400";
    # ETag 由 Nginx 自动生成,一天后走协商
}

策略说明:

  • index.html:用 no-cache,每次访问都向服务器验证,确保拿到最新入口
  • app-a1b2c3.js 等:文件名包含内容 hash,内容变 → hash 变 → URL 变 → 请求新文件。所以可以放心设置 1 年强缓存 + immutable
  • logo.png:没有 hash 无法通过 URL 变化来更新,所以用短一些的强缓存(1 天),过期后走协商缓存

优化建议:给 logo.png 也加上 hash(在 Vite 配置中修改),这样就能享受超长强缓存了。

第三题(⭐⭐⭐ 综合):排查缓存问题

用户反馈:发布新版本后,部分用户看到的仍然是旧页面,即使刷新也不行。你的部署方式是:

  1. 构建产出 index.html + assets/xxx.hash.js
  2. 先上传静态资源到 CDN
  3. 再更新 index.html

请分析可能的原因,并给出解决方案。

点击查看答案与解析

可能原因:

  1. HTML 被 CDN 或浏览器强缓存了。如果 index.html 被设置了 max-age(哪怕很短),在缓存期内用户拿到的都是旧 HTML,引用的是旧 JS/CSS。

  2. 部署时序问题:先上传新的静态资源,再更新 HTML。在两步之间的时间窗口里,旧 HTML 引用旧资源 URL,但旧资源已经被新资源覆盖了(如果 CDN 没做版本化)。或者新 HTML 上去了但 CDN 的 HTML 缓存还没刷新。

  3. CDN 缓存未刷新:即使源站更新了 HTML,CDN 的边缘节点可能还缓存着旧版本。

解决方案:

  1. HTML 使用 Cache-Control: no-cache,绝不设置强缓存
  2. CDN 对 HTML 设置短 TTL 或 no-cache
  3. 部署顺序调整
    • 先上传新的静态资源(因为有 hash,新旧文件并存,互不影响)
    • 再更新 HTML
    • 最后清理旧的静态资源(可选,可以保留一段时间兼容还在访问旧 HTML 的用户)
  4. 部署后主动刷新 CDN 缓存(purge)
  5. 静态资源采用”只增不删”策略:新版本的 JS/CSS 文件名带新 hash,旧文件不删除。这样即使用户拿到了旧 HTML,引用的旧 JS/CSS 仍然可以从 CDN 获取,不会 404

核心原则:先部署可以缓存的(静态资源),后部署不缓存的(HTML)。


自我检测

  • 能画出 HTTP 缓存的完整流程图(强缓存 → 协商缓存)
  • 能说清楚 Cache-Controlmax-ageno-cacheno-store 三者的区别
  • 能解释 ExpiresCache-Control 的关系和优先级
  • 能说出 ETagLast-Modified 的优缺点及优先级
  • 能为不同类型的资源(HTML、带 hash 的 JS/CSS、API)设计合适的缓存策略
  • 能解释 200 (from cache)304 Not Modified 的区别
  • 能说出 Service Worker 缓存和 HTTP 缓存的关系
  • 能排查常见的缓存相关问题(发布后用户看到旧版本等)

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90