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

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

相关推荐
2503_928411562 小时前
9.26 数据可视化
前端·javascript·信息可视化·html5
我叫唧唧波2 小时前
【打包工具】webpack基础
前端·webpack
知识分享小能手4 小时前
React学习教程,从入门到精通,React 单元测试:语法知识点及使用方法详解(30)
前端·javascript·vue.js·学习·react.js·单元测试·前端框架
PineappleCoder7 小时前
搞定用户登录体验:双 Token 认证(Vue+Koa2)从 0 到 1 实现无感刷新
前端·vue.js·koa
EveryPossible8 小时前
展示内容框
前端·javascript·css
伊织code8 小时前
WebGoat - 刻意设计的不安全Web应用程序
前端·安全·webgoat
子兮曰8 小时前
Vue3 生命周期与组件通信深度解析
前端·javascript·vue.js
拉不动的猪8 小时前
回顾关于筛选时的隐式返回和显示返回
前端·javascript·面试
yinuo8 小时前
不写一行JS!纯CSS如何读取HTML属性实现Tooltip
前端
gnip9 小时前
脚本加载失败重试机制
前端·javascript