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

本文将带你用最通俗的语言,彻底搞懂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服务像冰箱一样,既保鲜又节能,缓存用得好,用户体验少不了!😎

相关推荐
YGY_Webgis糕手之路5 分钟前
OpenLayers 综合案例-切片坐标与经纬网调试
前端·gis
白白白鲤鱼7 分钟前
Vue2项目—基于路由守卫实现钉钉小程序动态更新标题
服务器·前端·spring boot·后端·职场和发展·小程序·钉钉
xianxin_22 分钟前
HTML5 地理定位
前端
dong__csdn22 分钟前
nginx代理出https,request.getRequestURL()得到http问题解决
网络·http
Running_C29 分钟前
Vue组件化开发:从基础到实践的全面解析
前端·vue.js·面试
Clain30 分钟前
如何搭建一台属于自己的服务器并部署网站,超详细小白教程
linux·运维·前端
胡清波32 分钟前
小程序中使用字体图标的最佳实践
前端
xianxin_33 分钟前
HTML5 客户端存储
前端
南玖i34 分钟前
使用vue缓存机制 缓存整个项目的时候 静态的一些操作也变的很卡,解决办法~超快超简单~
前端·javascript·vue.js
计算机毕设定制辅导-无忧学长1 小时前
InfluxDB 集群部署与高可用方案(二)
java·linux·前端