本文将带你用最通俗的语言,彻底搞懂HTTP缓存的强缓存与协商缓存机制,配合完整代码和优缺点分析,让你的Node.js服务飞起来!🛫
一、HTTP缓存知识体系梳理
1. 强缓存(Cache-Control: max-age)
- 定义:浏览器在缓存有效期内直接使用本地副本,不请求服务器。
- 关键响应头 :
Cache-Control: max-age=86400
- 典型场景:图片、JS、CSS等静态资源。
- 优点:省流量、快如闪电。
- 缺点:资源更新不及时,用户可能吃到"过期菜"。
2. 协商缓存(Last-Modified / ETag)
- 定义:浏览器每次请求都带上资源标识,服务器判断资源是否变更。
- 关键响应头 :
Last-Modified
/If-Modified-Since
ETag
/If-None-Match
- 优点:资源变更能及时感知。
- 缺点:多一次请求,略慢。
二、完整代码演示与详细讲解
1. Last-Modified 协商缓存代码完整示例
javascript
const http = require('http')
const path = require('path')
const fs = require('fs')
const mime = require('mime')
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', req.url))
if (fs.existsSync(filePath)) {
let stat = fs.statSync(filePath)
if (stat.isDirectory()) {
filePath = path.resolve(filePath, 'index.html')
}
if (fs.existsSync(filePath)) {
const { ext } = path.parse(filePath)
const timeStamp = req.headers['if-modified-since']
let status = 200
if (timeStamp && Number(timeStamp) === stat.mtimeMs) {
status = 304
}
const contentType = mime.getType(ext)
res.writeHead(status, {
'Content-Type': contentType,
'Cache-Control': 'max-age=86400',
'Last-Modified': stat.mtimeMs
})
if (status === 200) {
const fileStream = fs.createReadStream(filePath)
fileStream.pipe(res)
} else {
res.end()
}
}
} else {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('<h1>404 Not Found</h1>')
}
})
server.listen(3000, () => {
console.log('server is running at http://localhost:3000');
})
代码讲解
- 通过
Last-Modified
和If-Modified-Since
实现协商缓存。 - 服务器将文件的
mtimeMs
(最后修改时间)放入Last-Modified
响应头。 - 浏览器下次请求时带上
If-Modified-Since
,服务器判断是否返回304。
2. 强缓存与ETag协商缓存代码示例
javascript
const http = require('http')
const path = require('path')
const fs = require('fs')
const mime = require('mime')
const checksum = require('checksum')
const server = http.createServer((req, res) => {
let filePath = path.resolve(__dirname, path.join('www', req.url))
if (fs.existsSync(filePath)) {
let stat = fs.statSync(filePath)
if (stat.isDirectory()) {
filePath = path.resolve(filePath, 'index.html')
}
if (fs.existsSync(filePath)) {
const { ext } = path.parse(filePath)
checksum.file(filePath, (err, sum) => {
const resStream = fs.createReadStream(filePath)
sum = `"${sum}"`
if (req.headers['if-none-match'] === sum) {
res.writeHead(304, {
'Content-Type': mime.getType(ext),
'Cache-Control': 'max-age=86400',
'etag': sum
})
res.end()
} else {
res.writeHead(200, {
'Content-Type': mime.getType(ext), charset: 'utf-8',
'Cache-Control': 'max-age=86400',
'etag': sum
})
resStream.pipe(res)
}
})
}
} else {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('<h1>404 Not Found</h1>')
}
})
server.listen(3000, () => {
console.log('server is running at http://localhost:3000');
})
代码讲解
- 通过
Cache-Control
实现强缓存,浏览器在有效期内不会请求服务器。 - 通过
ETag
实现协商缓存,资源内容变更时自动更新etag,浏览器带If-None-Match
头请求,服务器判断是否返回304。 - 代码中用
checksum
模块生成文件指纹,保证etag的唯一性。
📸 效果截图
首次访问页面
从图中的网络请求的状态码、大小和时间可以看出 首次请求的数据是来自指定的文件夹中的内容数据

刷新页面(再次访问页面)
相比较于第一次访问页面, localhost请求状态变为304, 大小也缩减了大半。js文件和图片文件的大小直接来自于浏览器的磁盘缓存当中,所有请求的耗费的时间也显著减少

❓为什么localhost请求不是所有数据都来自于缓存呢
在协商缓存(如Last-Modified/ETag)命中时,浏览器会向服务器发起请求,服务器判断资源未变更后返回304状态码,响应体为空,浏览器会从本地缓存中读取文件内容渲染页面。但 请求和响应头部信息(如状态码、响应头)依然是新获取的 ,并非全部内容都直接来自缓存。 此外,部分资源(如html文件)即使命中协商缓存,浏览器也会重新处理请求流程,包括网络面板显示的时间、大小等信息,这些是本次请求的元数据,而不是缓存中的旧数据。只有响应体内容才真正复用缓存。 简而言之:
- 304响应下,内容来自本地缓存,但请求和响应的元信息是新生成的。
- 浏览器会显示一次新的请求记录,方便开发者调试和分析缓存命中情况。
这也是为什么你会看到html文件状态为304,但仍有部分数据不是直接"来自缓存",而是本次请求的上下文信息。
三、优缺点分析
缓存类型 | 优点 | 缺点 |
---|---|---|
强缓存 | 响应快、省带宽 | 资源更新不及时 |
协商缓存 | 资源变更能及时感知 | 多一次请求,略慢 |
生活化比喻
- 强缓存:就像你冰箱里有一瓶牛奶,保质期内你直接喝,不问超市。
- 协商缓存:每次喝之前,先打电话问超市"牛奶换新了吗?",没换就继续喝。
四、最佳实践与建议
- 静态资源优先用强缓存,配合文件名加hash(如
logo.xxxxx.png
),资源更新时自动失效。 - 动态资源或频繁变更的内容用协商缓存,保证数据新鲜。
- 合理设置
Cache-Control
、ETag
和Last-Modified
,让浏览器和服务器各司其职。
五、图片/图表预留位
- 📊缓存流程图
text
客户端发起请求
↓
服务器接收请求,解析URL获取目标文件路径
↓
检查文件是否存在?
├─ 不存在 → 返回404响应(无缓存逻辑)
└─ 存在 → 检查是否为目录?
├─ 是 → 自动指向该目录下的index.html
└─ 否 → 处理文件缓存逻辑
↓
计算文件的checksum(作为ETag值,格式为"sum")
↓
检查请求头中是否有if-none-match?
├─ 有 → 比较if-none-match值与当前ETag是否一致?
│ ├─ 一致(缓存命中) → 返回304 Not Modified响应
│ │ (响应头包含:ETag、Cache-Control: max-age=86400)
│ └─ 不一致(缓存失效) → 返回200 OK响应
│ (响应头包含:文件内容、ETag、Cache-Control: max-age=86400)
└─ 无(首次请求,无缓存) → 返回200 OK响应
(响应头包含:文件内容、ETag、Cache-Control: max-age=86400)
↓
客户端接收响应
├─ 304响应 → 使用本地缓存的文件
└─ 200响应 → 缓存文件及ETag(有效期1天,由max-age指定)
- 🖼️首次请求时序图(客户端无缓存)
客户端 | 时间线 | 服务器 |
---|---|---|
1. 发送请求(无 if-none-match 头) | → | 2. 接收请求,解析文件路径 |
← | 3. 计算文件 ETag 为 "新 ETag" | |
← | 4. 返回 200 响应: 响应体包含文件内容, 响应头包含 ETag="新 ETag"、Cache-Control: max-age=86400 | |
5. 收到 200,缓存文件及 ETag |
- 🖼️缓存命中时序图(客户端已有缓存且有效)
客户端 | 时间线 | 服务器 |
---|---|---|
1. 发送请求(带请求头: if-none-match: "旧 ETag") | → | 2. 接收请求,解析文件路径 |
← | 3. 计算文件当前 ETag 为 "旧 ETag" | |
← | 4. 比较一致,返回 304 响应: 响应头包含 ETag、Cache-Control: max-age=86400 | |
5. 收到 304,使用本地缓存文件 |
- 📊缓存失效时序图(客户端缓存过期或 ETag 不匹配)
客户端 | 时间线 | 服务器 |
---|---|---|
1. 发送请求(无 if-none-match 头) | → | 2. 接收请求,解析文件路径 |
← | 3. 计算文件 ETag 为 "新 ETag" | |
← | 4. 返回 200 响应: 响应体包含文件内容, 响应头包含 ETag="新 ETag"、Cache-Control: max-age=86400 | |
5. 收到 200,缓存文件及 ETag |
六、参考资料
让你的Node.js服务像冰箱一样,既保鲜又节能,缓存用得好,用户体验少不了!😎