HTTP 缓存深度解析:强缓存与协商缓存实战指南
在构建高性能 Web 应用时,HTTP 缓存 是提升用户体验、降低服务器负载的核心技术。它通过让浏览器智能地复用之前获取的资源,避免了不必要的网络请求和数据传输。深入理解并正确运用强缓存 (Strong Cache)和协商缓存(Negotiation Cache),是每个 Web 开发者必备的技能。
本文将系统性地讲解这两种缓存机制的原理、关键 HTTP 头部、为什么使用 、何时使用 以及具体的应用场景,并通过 JavaScript 代码示例帮助你掌握实践。
核心目标:减少请求,加速访问
缓存的根本目的只有一个:当用户再次请求同一资源时,尽可能避免从源服务器重新下载完整的数据。理想状态是资源直接从用户的内存或磁盘中读取(最快),次优是从 CDN 节点获取。强缓存和协商缓存是实现这一目标的两种互补策略。
1. 强缓存 (Strong Cache) - "有效期之内,我说了算"
核心思想 :浏览器根据服务器在首次响应 中设定的"有效期",完全自主地判断 本地缓存是否"新鲜"。只要在有效期内,浏览器就不向服务器发送任何请求 ,直接使用本地副本。这是性能最优的缓存方式。
关键响应头字段
Cache-Control
(HTTP/1.1, 优先级最高) :max-age=<seconds>
:指定资源在客户端缓存中保持"新鲜"的最大时间(秒)。例如max-age=3600
表示 1 小时内可直接使用缓存。public
:响应可以被任何缓存(浏览器、CDN、代理)存储。private
:响应只能被单个用户的浏览器缓存。no-cache
:极具误导性! 它不是 "不缓存"。它的意思是:"资源可以缓存,但在使用前,必须向服务器发起一个验证请求(即进入协商缓存) "。它禁用了强缓存。no-store
:真正的"禁止缓存"。任何缓存都不能存储该响应。s-maxage=<seconds>
:仅适用于共享缓存(如 CDN)。
Expires
(HTTP/1.0, 优先级低于Cache-Control
) :- 指定一个绝对的过期时间 (GMT 格式)。如果
Cache-Control: max-age
存在,则忽略Expires
。
- 指定一个绝对的过期时间 (GMT 格式)。如果
为什么使用强缓存?
- 极致性能 :完全避免网络请求,无任何网络延迟,资源加载接近瞬时。
- 提升用户体验:用户重复访问或页面跳转时,静态资源瞬间呈现。
- 降低服务器压力:服务器无需处理这些资源的请求。
- 节省用户流量:避免重复下载相同文件。
何时使用?应用场景与 JS 实现
原则 :适用于内容在部署后长期不变,或通过文件名哈希确保版本更新的静态资源。
应用场景 1:带哈希的静态资源 (现代前端最佳实践)
-
场景描述 :使用 Webpack、Vite 等构建工具,JS、CSS、图片等文件会被生成带内容哈希的文件名,如
main.a1b2c3d4.js
。 -
为什么用:文件内容改变时,哈希值必然改变,导致文件名改变。旧文件名的资源可以永久缓存,新文件名的资源被视为全新资源。
-
如何在 Node.js 服务器中配置 :
javascriptconst express = require('express'); const path = require('path'); const app = express(); // 为构建后的静态资源目录设置强缓存 app.use(express.static(path.join(__dirname, 'dist'), { // 为所有静态文件设置 1 年的 max-age setHeaders: (res, filePath) => { // 只对 JS、CSS、图片、字体等设置长缓存 if (/\.(js|css|png|jpg|jpeg|gif|ico|webp|svg|woff|woff2|ttf|eot)$/.test(filePath)) { res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1 year } // HTML 文件不在此处设置,单独处理 } })); // 处理所有其他请求,返回 index.html (用于 SPA) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); app.listen(3000);
-
效果 :用户首次访问,下载
main.a1b2c3d4.js
。后续访问,浏览器看到max-age=31536000
有效,直接使用本地缓存,不发请求 。部署新版本后,main.x9y8z7w6.js
是新文件名,浏览器会请求新文件。
应用场景 2:更新不频繁的配置文件
-
场景描述 :一个
config.json
文件,包含应用常量,更新频率低。 -
为什么用:内容稳定,但不像带哈希的文件那样有强制刷新机制。
-
如何配置 :
javascript// 假设有一个 /config 的 API 端点 app.get('/config', (req, res) => { const config = { /* 配置数据 */ }; // 设置较短的强缓存,例如 1 小时 res.setHeader('Cache-Control', 'public, max-age=3600'); res.json(config); });
-
效果:用户1小时内刷新,直接使用本地缓存。1小时后,强缓存失效,进入协商缓存流程。
何时避免使用?
- 内容频繁变化且文件名不变 :如
app.js
每天更新但文件名不变。 - 实时性要求极高:如股票行情、聊天消息。
- 敏感数据 :如用户隐私信息,应使用
no-store
。
2. 协商缓存 (Negotiation Cache) - "我先确认一下,再决定"
核心思想 :当强缓存失效(或被 no-cache
强制)时,浏览器会发起一个轻量级的"条件请求",询问服务器:"我本地有这个版本,它还有效吗?" 服务器只需进行简单比对:
- 如果有效 ,返回
304 Not Modified
(无响应体),浏览器使用本地缓存。 - 如果无效 ,返回
200 OK
和新的资源。
关键请求/响应头字段
ETag
/If-None-Match
:ETag
(响应头):服务器为资源生成的唯一标识符 (通常是内容的哈希)。内容变,ETag
变。If-None-Match
(请求头):浏览器将之前收到的ETag
值发给服务器验证。
Last-Modified
/If-Modified-Since
:Last-Modified
(响应头):资源的最后修改时间(GMT)。If-Modified-Since
(请求头):浏览器将之前收到的时间发给服务器。
为什么使用协商缓存?
- 保证新鲜度:解决了强缓存在有效期内无法感知更新的问题。
- 节省带宽 :验证通过时,服务器只返回
304
状态码,不传输资源本体。 - 降低服务器开销:服务器无需读取和传输大文件。
- 适用于更新不确定的资源:是强缓存失效后的优雅降级。
何时使用?应用场景与 JS 实现
原则 :适用于内容会更新,需要保证一定时效性,且希望避免全量下载的资源。
应用场景 1:HTML 文件 (关键入口)
-
场景描述 :网站的
index.html
或article.html
。它是页面的入口,包含了对 JS、CSS 的引用。 -
为什么用:如果 HTML 被强缓存太久,即使新的 JS/CSS 已部署,用户加载的旧 HTML 可能仍引用旧的文件名,导致无法看到更新。但每次都下载完整的 HTML 代价也高。
-
如何在 Node.js 服务器中配置 :
javascriptconst fs = require('fs'); const crypto = require('crypto'); // 读取 HTML 文件的 ETag (例如基于文件内容的 MD5) function getETagForFile(filePath) { const content = fs.readFileSync(filePath); return crypto.createHash('md5').update(content).digest('hex'); } app.get('*', (req, res) => { const indexPath = path.join(__dirname, 'dist', 'index.html'); // 为 HTML 文件设置 no-cache,强制协商 res.setHeader('Cache-Control', 'no-cache'); // 生成 ETag const etag = getETagForFile(indexPath); // 检查协商请求头 if (req.headers['if-none-match'] === etag) { // 资源未变 res.status(304).end(); return; } // 资源有变或首次请求 res.setHeader('ETag', etag); res.sendFile(indexPath); });
-
效果 :用户刷新页面,浏览器发起包含
If-None-Match
的请求。如果 HTML 未变,服务器返回304
,浏览器用本地缓存,页面快速加载。只有 HTML 真正更新时,才返回200
和新内容。
应用场景 2:API 数据 (动态内容)
-
场景描述 :获取用户信息
GET /api/user/123
。 -
为什么用:数据会更新,但短时间内刷新内容可能没变。
-
如何在 Node.js 服务器中配置 :
javascript// 模拟数据库 const users = { 123: { name: 'Alice', lastUpdated: Date.now() } }; // 生成用户数据的 ETag (例如基于数据和更新时间的哈希) function generateUserETag(user) { const dataStr = JSON.stringify(user) + user.lastUpdated; return crypto.createHash('sha1').update(dataStr).digest('hex'); } app.get('/api/user/:id', (req, res) => { const user = users[req.params.id]; if (!user) { return res.status(404).end(); } // 生成 ETag const etag = generateUserETag(user); // 检查协商请求头 if (req.headers['if-none-match'] === etag) { res.status(304).end(); return; } // 资源有变或首次请求 res.setHeader('ETag', etag); // 通常 API 也设置 no-cache 或短 max-age res.setHeader('Cache-Control', 'no-cache'); res.json(user); });
-
效果 :用户短时间内多次请求用户信息,大部分情况下服务器返回
304
,前端使用缓存数据,体验流畅。
强缓存与协商缓存:协同工作
它们不是对立的,而是协作构成一个完整的缓存生命周期:
- 优先检查强缓存 :看
Cache-Control
/Expires
是否有效。- 有效:直接使用本地缓存。
- 失效 或
no-cache
:进入下一步。
- 发起协商请求 :发送
If-None-Match
/If-Modified-Since
。 - 服务器验证 :
304 Not Modified
:使用本地缓存,并重置强缓存计时器。200 OK
:下载新资源,更新本地缓存。
使用本地缓存] G -- 资源已变 --> I[返回 200 + 新资源]
总结与最佳实践
资源类型 | 推荐策略 | 配置要点 | 说明 |
---|---|---|---|
带哈希的 JS/CSS | 强缓存 | Cache-Control: public, max-age=31536000 |
构建工具生成,可长期缓存。 |
图片/字体 | 强缓存 | Cache-Control: public, max-age=31536000 |
内容稳定,长期缓存。 |
HTML 文件 | 协商缓存 | Cache-Control: no-cache + ETag |
入口文件,必须能验证更新。 |
API 数据 | 协商缓存 | Cache-Control: no-cache + ETag |
保证数据新鲜,节省带宽。 |
高度敏感数据 | 禁用缓存 | Cache-Control: no-store |
绝对不缓存。 |
关键要点:
no-cache
≠ 不缓存:它是协商缓存的触发器。- 文件名哈希是强缓存的基石:它让长期缓存变得安全可靠。
- HTML 是缓存策略的枢纽 :通常用
no-cache
确保其能及时更新。 ETag
优于Last-Modified
:更精确,能检测秒级内的修改。- 监控与测试 :使用浏览器开发者工具的 Network 面板,观察
Status
(200, 304, from cache) 和Size
列,验证策略是否生效。
通过精心设计和配置强缓存与协商缓存,你可以构建出既快速又可靠的 Web 应用,为用户提供卓越的体验。