「浏览器缓存」让费曼告诉你他到底是什么

72 阅读11分钟

🚀 栏目使用说明书

1️⃣ 框架固定:本文确立「问题清单→费曼复盘→深度解析→扩展阅读」四步学习法,后续文章均按此结构展开
2️⃣ 进化秘笈:如果你有更好的创意→📢 评论区 拍砖/建议/脑洞,你说了我就(bu)改!
3️⃣ 食用姿势:建议按照「自测→思考→对照→拓展」顺序服用,效果更佳

既然是面试系列,那么首要问题就是根据相关知识点,列出面试官可能会提出的问题,大家可以就题目进行思考,看下如果是自己该如何作答,是否能回答出来,回答的思路是否清晰。题目列出后会根据对应的问题应用费曼学习法将学习到的内容进行输出。

文章末会贴出相关的参考资料供大家在疑惑时参考官方文档进行理解和学习~~~

🎮 新手村任务:认识这个叫"缓存"的NPC

缓存(Cache)  是一种临时存储数据的技术,目的是通过保存常用或最近访问的数据副本,减少访问原始数据源(如硬盘、数据库、网络)的次数,从而提升系统性能、降低延迟并节省资源。

HTTP 缓存的运作方式 浏览器发出的所有 HTTP 请求都会先路由到浏览器缓存,以检查是否有可用于执行请求的有效缓存响应。如果有匹配项,系统会从缓存中读取响应,从而消除网络延迟和传输产生的数据费用。

默认情况下,HTTP 缓存以 URL 为唯一标识

  • 相同 URL:浏览器或代理服务器会认为请求的是同一资源,直接返回缓存副本(若未过期)。

  • 不同 URL:即使内容相同(如不同路径下的重复资源),也会被视作独立资源,分别缓存。

🛡️  【青铜试炼】基础必杀题

  1. 缓存分类
  • 浏览器缓存分为哪两类?它们的核心区别是什么?
  1. HTTP 头部字段
  • 列举常用的缓存相关 HTTP 头部字段,并说明其作用。
  • Cache-Control有哪些常见取值?max-ageExpires的区别是什么?
  1. 缓存工作流程
  • 描述浏览器请求资源时的缓存判断流程(从强缓存到协商缓存)。

⚔️  【王者对线】高阶实战题

  1. 缓存策略选择
  • 静态资源(如 CSS/JS/ 图片)和动态内容(如 API 响应)应如何设置缓存策略?为什么?
  • 如何实现 “永不缓存” 某个资源?如何强制浏览器更新缓存?
  1. 缓存失效场景
  • 哪些情况下浏览器会跳过缓存直接请求服务器?(如用户手动刷新)
  • 如果修改了静态资源内容但文件名未变,如何让浏览器获取最新版本?
  1. 缓存与 CDN
  • CDN 的缓存机制与浏览器缓存有何关联?如何通过 HTTP 头控制 CDN 缓存?

🕵️ 【隐藏副本】死亡追问方向

  • 当候选人提到 ETag 时
    ETag 和 Last-Modified 的区别是什么?为什么 ETag 更精确?在什么场景下会选择使用 ETag?

  • 当候选人提到协商缓存时
    协商缓存中,服务器返回 304 状态码的含义是什么?此时浏览器如何处理缓存?

  • 当候选人提到缓存策略冲突时
    如果同时设置了Cache-ControlExpires,哪个优先级更高?为什么?

  • 当候选人提到缓存问题时
    实际项目中遇到过哪些缓存导致的问题?如何排查和解决?(例如旧代码未更新)

💡费曼解答时刻

一、浏览器缓存的类型

浏览器缓存的类型一共有两种,分别是强缓存协商缓存,也可以称为本地缓存和代理缓存,区别在于存取的过程和方式。

  • 执行顺序: 强缓存 -> 协商缓存,浏览器会先判断当前是否为强缓存,不为强缓存时再去判断是否为协商缓存
  • 缓存方式:强缓存和协商缓存都是通过响应头的参数进行判断,并根据参数进行缓存操作。

注意: 浏览器缓存只会发生在第一次获取指定资源之后,即获取到响应头后才会根据相应策略进行缓存。 具体流程如图:

graph TD
    A[发起请求] --> B{是否有缓存?}
    B -->|无缓存| C[直接向服务器请求]
    B -->|有缓存| D{是否过期?}
    D -->|未过期| E[使用缓存 200]
    D -->|已过期| F[携带缓存标识请求服务器]
    F --> G{资源是否修改?}
    G -->|未修改| H[304 使用缓存]
    G -->|已修改| I[返回新资源 200]

二、缓存类型与常用的相关 HTTP 头部字段

浏览器在处理所有 HTTP 请求时,首先会检查浏览器缓存中是否存在有效的缓存响应。缓存机制通过请求头和响应头中的特定字段协同控制,具体分为以下两种策略:

🔧 强缓存策略(无需与服务器确认)

控制强缓存的策略一共有两个响应头属性:ExpiresCache-Control

  • Cache-Control中的max-age属性与Expires的作用一致,都是指明当前资源的有效期

  • Cache-Control的优先级要比Expires高,当两个属性都有值的情况下,优先以Cache-Control为准。

  • Expires 标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。

  • Cache-Control 采用了 max-age——用于指定经过的时间。

  • Cache-Control有多种指令可选择,并且可以同时设置多种指令。因此建议优先设置Cache-Control指令来控制强缓存,Expires作为一种兼容性的补充。

  • Cache-Control通用字段,可设置多个指令:

    • max-age=3600:资源缓存有效期(秒)。
    • no-cache必须向服务器验证缓存是否过期(即使用协商缓存)。
    • no-store禁止缓存(敏感数据场景)。
    • public:允许代理服务器缓存资源。
    • private:仅允许客户端(浏览器)缓存资源。
    • s-maxage=300:代理服务器缓存的有效期(覆盖 max-age)。

💡Expires设置示例

// 计算并设置 Expires 头,这里设置为当前时间的 1 小时后
const now = new Date();
const expires = new Date(now.getTime() + 3600 * 1000);
res.setHeader("Expires", expires.toUTCString());

Expires 是 HTTP 1.0 的规范,它依赖于客户端和服务器的时间同步。如果客户端和服务器的时间不一致,可能会导致缓存策略失效。
💡Cache-Control设置示例

// 计算并设置 Cache-Control 头,这里设置为缓存 1 小时
const maxAge = 60 * 60; // 
res.setHeader('Cache-Control', `max-age=${maxAge}`);

在现代 Web 开发中,更推荐使用 Cache-Control 头的 max-age 指令来控制强缓存,因为它是 HTTP 1.1 的规范,相对更可靠。不过 Expires 仍然可以作为一种兼容性的补充。

🔄 协商缓存策略(需与服务器验证)

过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这个验证过程也称为协商缓存。

验证是通过使用包含 If-Modified-Since 或 If-None-Match 请求标头的条件请求完成的。

但是协商缓存的过期时间和策略依然是在服务端中设置,即通过响应头的ETagLast-Modified标头进行判断。

  • 当与 If-Modified-Since 一同使用的时候,If-None-Match 优先级更高
  • If-Modified-Since 给定的日期时间,超出该日期则为过期。通过Last-Modified响应标头获取。
  • If-None-Match 通常是文件内容的哈希值,通过ETage响应标头获取。
  • ETag 属性之间的比较采用的是弱比较算法,即两个文件除了每个字节都相同外,内容一致也可以认为是相同的。

💡Last-Modeified设置示例

  const filePath = path.join(__dirname, "index.html");
  const stats = fs.statSync(filePath);
  const lastModified = stats.mtime.toUTCString();
  // 设置 Last-Modified
  res.setHeader("Last-Modified", lastModified);

💡ETag设置示例

 // 计算文件的 ETag
 const hash = crypto.createHash("sha256").update(data).digest("hex");
 // 设置 ETag
 res.setHeader("ETag", hash);

三、缓存策略选择

静态资源(如 CSS/JS/ 图片)和动态内容(如 API 响应)应如何设置缓存策略?

静态资源(如前端打包出来的产物 CSS/JS/ IMG)

在现代的构建工具中,如vite和webpack,他们在构建过程中会生成带哈希的文件名(如 logo.a1b2c3d4.png),确保内容更新时 URL 变化,强制客户端获取新版本。因此这类资源我们可以通过Nginx配置以下的缓存策略使其最大化缓存利用率,减少重复请求。

1. 强缓存 + 长期有效期

location /static/ {
 add_header Cache-Control "public, max-age=31536000, immutable";
 # 文件名哈希化(通过 Webpack/Vite 等工具实现)
}

特性分析

  • 内容稳定性:静态资源一旦发布,通常不会频繁修改。
  • 性能优先:通过长期缓存减少网络请求,显著提升页面加载速度。
  • 版本控制:文件名哈希化解决了缓存更新问题(旧版本 URL 自动失效)。

动态资源(如 API 响应)

为了保证数据的实时性和一致性,避免返回过期信息,动态资源的缓存策略一般是禁用缓存。

1. 设置 Cache-Control 头部,实现“永不缓存”资源

app.get("/api/user", (req, res) => {
  res.set("Cache-Control", "no-store", "no-cache, "must-revalidate");
});
  • no-store:禁止浏览器和代理服务器缓存资源(最严格的策略)。
  • no-cache:每次请求必须向服务器验证缓存是否有效。
  • must-revalidate:缓存过期后必须重新验证。

四、哪些情况下浏览器会跳过缓存直接请求服务器

1. HTTP 头部明确禁止缓存

这里主要是采取了上面所讲的禁止缓存策略,因此每次请求都直接访问服务器。

2. 用户主动操作

强制刷新页面

  • 操作:按下 Ctrl + F5(Windows)或 Cmd + Shift + R(Mac)。

开发者工具禁用缓存

  • 操作:在浏览器开发者工具中勾选  “Disable cache”
  • 行为:所有请求自动添加 Cache-Control: no-cache,强制从服务器获取资源。

3. 缓存过期或失效

  • Cache-Control: max-age=3600 或 Expires 时间已过
  • 服务器资源已更新,ETag 或 Last-Modified 与客户端提交的值不匹配。

五、修改了静态资源内容但文件名未变,如何让浏览器获取最新版本?(例如旧代码未更新)

这种情况经常发生在SPA(单页面应用)的项目中,因为SPA项目核心是单一 HTML 文件(如index.html),所有路由和内容都通过 JavaScript 动态渲染。因此当有版本迭代时,用户在已打开的页面中是无法感知的,路由的跳转和URL参数的修改解决不了缓存的问题,因此我们需要做出相应的策略去让用户感知到新版本的存在。

1. 强制刷新(前端代码)

// 示例:通过服务端接口获取版本号
fetch('/version').then(res => res.json()).then(version => {
  if (version !== currentVersion) {
    location.reload(); // 触发浏览器强制刷新
  }
});

🔍 扩展阅读:浏览器缓存深度解析手册

🌐 缓存机制全景图

stateDiagram-v2
    [*] --> 强缓存检查
    强缓存检查 --> 有效: Cache-Control/Expires未过期
    有效 --> [*]: 200 (from cache)
    强缓存检查 --> 失效: 缓存过期
    失效 --> 协商缓存验证: 携带If-None-Match/If-Modified-Since
    协商缓存验证 --> 未修改: ETag匹配
    未修改 --> [*]: 304 (Not Modified)
    协商缓存验证 --> 已修改: ETag不匹配
    已修改 --> [*]: 200 (新资源)

💡参考回答思路

  • 强缓存:通过Cache-ControlExpires控制,直接读取本地缓存,状态码为 200(from disk/memory cache)。
  • 协商缓存:通过Last-Modified/If-Modified-SinceETag/If-None-Match验证资源是否新鲜,返回 304 时使用缓存。
  • 实际应用:静态资源设置长max-age,动态接口设置no-cache并配合must-revalidate

🛠️ HTTP 头部字段详解表

字段类型典型值示例作用场景优先级
Cache-Control响应头max-age=3600, public现代缓存控制核心⭐⭐⭐⭐
Expires响应头Expires: Wed, 21 Oct 2023 07:28:00 GMT兼容旧系统的过期时间控制⭐⭐
ETag响应头W/"d3b07384d113edec49eaa6238ad5ff00"精确内容验证(如API数据)⭐⭐⭐⭐
Last-Modified响应头Last-Modified: Tue, 15 Aug 2023 12:45:26 GMT粗略时间验证(静态文件)⭐⭐
Vary响应头Vary: User-Agent, Accept-EncodingCDN多版本缓存区分依据⭐⭐⭐

💡 高频面试题闪电战

Q1:强缓存 vs 协商缓存的核心区别?

🚀 闪电回答

  • 强缓存:不联系服务器,直接给缓存(200 from cache)
  • 协商缓存:必须问服务器资源是否变化(304/200)
    ⚡ 加分项:Chrome DevTools 的 Size 列显示 (disk cache) 即为强缓存命中

Q2:如何让修改后的JS文件立即生效?

🚀 三步解决方案

  1. 文件名哈希app.a1b2c3.js → 修改内容自动变哈希
  2. 查询参数法app.js?v=1.2.3
  3. 禁用缓存头Cache-Control: no-cache

Q3:用户按F5和Ctrl+F5的区别?

操作缓存行为网络面板特征
普通刷新使用强缓存+协商缓存大量304状态码
强制刷新完全绕过缓存所有请求200
回车访问优先强缓存from disk cache 提示

🚨 常见踩坑场景

场景:用户总看到旧版页面
💡 破局方案

  1. HTML文件设置 Cache-Control: no-cache
  2. 静态资源添加内容哈希
  3. 部署后CDN刷新缓存

📚 参考资料藏宝图

🔗 MDN Web Docs - HTTP缓存
🗺️ Google开发者-缓存策略

如果以上有错误的理解或者描述,欢迎大家批评指正!