缓存策略大乱斗:让你的页面快到飞起!

本文将带你用最通俗的语言,彻底搞懂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-ModifiedIf-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,但仍有部分数据不是直接"来自缓存",而是本次请求的上下文信息。


三、优缺点分析

缓存类型 优点 缺点
强缓存 响应快、省带宽 资源更新不及时
协商缓存 资源变更能及时感知 多一次请求,略慢

生活化比喻

  • 强缓存:就像你冰箱里有一瓶牛奶,保质期内你直接喝,不问超市。
  • 协商缓存:每次喝之前,先打电话问超市"牛奶换新了吗?",没换就继续喝。

四、最佳实践与建议

  1. 静态资源优先用强缓存,配合文件名加hash(如logo.xxxxx.png),资源更新时自动失效。
  2. 动态资源或频繁变更的内容用协商缓存,保证数据新鲜。
  3. 合理设置Cache-ControlETagLast-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服务像冰箱一样,既保鲜又节能,缓存用得好,用户体验少不了!😎

相关推荐
前端大卫20 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘36 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare37 分钟前
浅浅看一下设计模式
前端
Lee川40 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端