HTTP 缓存(也称 浏览器缓存)分为两大类:强缓存 和协商缓存。强缓存优先于协商缓存,强缓存命中时,浏览器直接使用本地副本,不会与服务器通信;若强缓存失效,则进入协商缓存阶段,向服务器验证资源是否更新。
HTTP 缓存特点
- 浏览器自动处理,无需 JS 干预。
- 遵循强缓存 和协商缓存规则。
- 存储位置:内存缓存(memory cache)和磁盘缓存(disk cache),由浏览器自行决定。
- 访问方式:浏览器自动读取,开发者无法通过 JS 直接读取缓存内容(只能通过 DevTools 查看)。
强缓存
强缓存是指浏览器直接从本地缓存中读取资源,而不需要向服务器发送请求。浏览器会根据缓存头信息来判断资源是否过期,如果未过期,就直接使用本地缓存的资源。
相关 HTTP 头:Expires(HTTP/1.0)和 Cache-Control(HTTP/1.1,优先级更高)。
请求头中的 Cache-Control
客户端利用 cache-control 向服务器或本地缓存提出要求 ,控制请求的行为(例如强制验证、限制使用过期缓存等)。不会直接"缓存"任何内容,而是告诉缓存如何处理已有的缓存或如何从服务器获取资源。
Cache-Control: no-cache(要求服务器不要返回缓存副本,必须重新验证)。max-age=0等同于no-cache。max-age = s, 客户端愿意接受的响应的最大生存时间(秒),超过则重新请求。max-stale = s, 客户端愿意接受过期缓存,过期时间不超过指定秒数(若不指定,任意过期也可接受)。min-fresh = s, 客户端要求缓存必须还有至少 s 秒的新鲜时间。Cache-Control: only-if-cached(若本地无缓存则返回 504)。客户端只接受已缓存的响应,不向源服务器发起网络请求。
响应头中的 Cache-Control
cache-control:public, 响应可以被任何缓存(包括 CDN、代理)缓存。private, 响应只能被终端浏览器缓存,不允许中间代理缓存(适用于用户个人数据)。no-store, 绝对禁止缓存(既不存磁盘也不存内存),每次都必须请求原始数据。no-cache, 缓存必须在使用前向源服务器验证(即使本地有副本也必须重新请求确认)。 5.max-age = s, 响应的最大新鲜时间(秒),从生成时间起算。 6.s-maxage = s, 针对共享缓存(如 CDN)的最大新鲜时间,优先级高于 max-age。must-revalidate, 缓存过期后必须向源服务器验证,不允许使用过期缓存。proxy-revalidate, 与 must-revalidate 类似,但仅适用于共享缓存。stale-while-revalidate = s, 缓存过期后在指定秒数内可继续返回过期内容,同时后台异步重新验证。stale-if-error = s,当源服务器出错时,可在指定秒数内使用过期缓存。immutable, 资源不会改变,可永久缓存(不验证),适合静态资源(如版本化的 CSS/JS)。no-transform, 禁止中间代理修改响应内容(如压缩、图片格式转换)。
示例 浏览器开启禁用缓存
GET 请求http://localhost:5177/api/home

示例 no-store 完全禁止任何形式的缓存
- 客户端(浏览器)不会将资源写入磁盘或内存缓存。
- 每次请求 :即使用户下次访问相同 URL,浏览器也必须重新向服务器请求完整资源,不会使用任何本地副本。
示例 no-cache
可以缓存,但每次使用前必须向服务器验证(协商缓存),不能直接使用强缓存。
示例 max-age=<seconds>
资源被视为"新鲜"的时长(从响应生成时刻起),单位秒。
example1 max-age = 0 / 同 no-cache
首次请求和二次请求
首次响应码返回 200, 二次返回 304 ,其中 304 响应的 Size 通常远小于 200 响应的 Size。
304 Not Modified :服务器返回仅包含响应头 ,没有响应体。它只是告诉客户端:"你请求的资源自上次缓存以来未发生变化,请直接使用本地的缓存副本。"

首次请求

二次请求
服务器会返回 304?当客户端发起条件请求时(例如带有 If-Modified-Since 或 If-None-Match 头部),服务器检查资源是否发生变化:
- 如果未变化 → 返回 304,无响应体,客户端使用本地缓存。
- 如果已变化 → 返回 200,同时发送新的完整资源。

example2 max-age = 60 有效期 60 秒
首次请求和二次请求 、过期后的第三次请求

当浏览器从磁盘缓存(disk cache)中直接读取资源并返回 200 状态码时,意味着该请求没有实际发起网络连接。因此:
- 没有 TCP 连接,自然也就没有
Connection ID(连接标识)。 - 浏览器开发者工具中 Network 面板的
Connection ID列会显示为空
首次请求
响应指令 cache-control:public, 响应可以被任何缓存(包括 CDN、代理)缓存。

有效期内 二次请求

过期后 请求

响应头 Expires
Expires 是 响应头(Response Header),由服务器在响应中发送给客户端,用于指示资源的过期时间。告诉浏览器在此绝对时间之前可以直接使用本地缓存,无需重新请求。

js
app.get("/api/logo-1", async (_, res) => {
// 设置 Expires 为 60 秒后
const expiresAt = new Date(Date.now() + 60 * 1000);
res.setHeader("Expires", expiresAt.toUTCString());
const file = await readFile(`${process.cwd()}/src/source/logo.png`, {});
res.send(file);
});

过期后请求

协商缓存
协商缓存是指浏览器在使用本地缓存之前,会先向服务器发送一个请求,询问服务器该资源是否有更新。如果服务器返回资源未更新的信息,浏览器就使用本地缓存;否则,就从服务器获取最新的资源。
强缓存失效后,浏览器向服务器发送请求,附上资源的"验证标记",由服务器判断资源是否更新。
- 若资源未更新 → 服务器返回 304 Not Modified,无响应体,浏览器继续使用旧缓存。
- 若资源已更新 → 服务器返回 200 OK 和新资源。
| 响应头 | 请求头(浏览器后续携带) | 说明 |
|---|---|---|
Last-Modified |
If-Modified-Since |
基于修改时间验证 |
ETag |
If-None-Match |
基于唯一标识(优先级高于修改时间) |
浏览器会自动在满足条件时加上协商缓存所需的请求头(If-None-Match / If-Modified-Since),这是浏览器的默认行为,无需开发者手动干预。
示例 ETag / If-None-Match HTTP/1.1
首次请求 GET 请求 http://localhost:5177/api/home
服务器返回资源,并附带 ETag 响应头

再次发起 GET 请求 http://localhost:5177/api/home
浏览器携带 If-None-Match 请求头,值为之前 响应头返回 Etag。
资源未变,客户端使用缓存。返回 304 Not Modified

示例 查看时间
GET 请求 http://localhost:5177/api/user
Queuing 1.39 ms, 请求在浏览器队列中等待。可能原因:有更高优先级的请求正在处理,或浏览器对同一域名并发连接数有限(HTTP/1.1 通常 6 个)。此值很小,正常。Stalled 2.12 ms, 请求已被创建但尚未发出,等待可用的网络连接。DNS Lookup 54 µs (0.054 ms), 域名解析时间。极短,说明 DNS 已被缓存(或主机名在本地 hosts 文件中),性能很好。Initial connection 0.78 ms, TCP 握手(+ TLS 握手,如果是 HTTPS)时间。非常快,表明与服务器物理距离近或连接被复用(Keep-Alive)。注意:如果使用了 HTTPS,这里会包含 TLS 开销Request sent 0.12 ms, 发送 HTTP 请求数据的时间。Waiting (TTFB) 34.15 ms, Time To First Byte,从发送完请求到接收到服务器响应的第一个字节。Content Download 0.45 ms, 从服务器下载响应体数据的时间。

Last-Modified / If-Modified-Since HTTP/1.0
浏览器在需要验证缓存时,会自动 根据上次服务器响应的 Last-Modified 值生成 If-Modified-Since 请求头,无需开发者手动编写代码。这是 HTTP 缓存机制的标准行为。