文章目录
HTTP缓存
缓存综述
缓存是什么
保存资源副本并在下次请求时直接使用该副本的技术。当Web缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载
为什么需要缓存
- 减少不必要的网络请求,使得页面加载更快;
- 网络请求是不稳定,加大了页面加载的不稳定性;
- 网络请求的加载相比于cpu加载 & 页面渲染都要慢
哪些资源可以被缓存
- 静态资源 js css img ,因为静态资源加上hash名打包后是不会修改的
- 默认 GET/HEADE 请求才会触发缓存逻辑
HTTP缓存策略
HTTP 缓存完整执行流程

浏览器首次请求资源:
- 发送 GET 请求→服务端返回 200 OK,携带资源体 + 强缓存头 + 协商缓存头→浏览器存储资源体和缓存标识(响应头)到本地缓存(内存 / 磁盘);
浏览器再次请求同一资源:
-
1 判断强缓存是否生效(根据Cache-Control: max-age或Expires):
✅ 命中:直接使用本地缓存,返回 200 (from cache),无网络请求;
❌ 未命中:进入第二步,发起协商缓存请求;
-
2 发起带缓存标识的 GET 请求(携带If-Modified-Since/If-None-Match)→服务端对比标识:
✅ 协商缓存命中:返回 304 Not Modified,无响应体→浏览器使用本地缓存;
❌ 协商缓存未命中:返回 200 OK,携带新资源 + 新缓存标识→浏览器更新本地缓存并使用新资源。
强缓存
浏览器直接通过本地缓存判断资源是否过期,无需发起 HTTP 请求到服务端,命中后直接使用本地缓存(内存 / 磁盘),返回状态码 200 OK (from memory / disk cache)
核心响应头(服务端返回,浏览器存储):
Cache-Control(HTTP/1.1 标准,优先级最高):支持多指令组合,常用指令
- max-age=xxx:资源有效期,单位秒,从请求成功开始计时
- public:所有缓存节点(浏览器、代理服务器如 Nginx)均可缓存
- private:仅浏览器可缓存,代理服务器不缓存(默认值)
- no-cache:非不缓存,跳过强缓存直接走协商缓存
- no-store:真正的不缓存,浏览器不存储任何资源,每次都发全新请求
- s-maxage=xxx:覆盖max-age,仅对代理服务器生效
本地node测试
ts
// 后端-Koa
// 1. 纯强缓存:Cache-Control(max-age=60,有效期60秒)
router.get('/cache/strong/cache-control', async (ctx) => {
ctx.set({
'Cache-Control': 'public, max-age=60', // 公网可缓存,有效期60秒
'Content-Type': 'application/json;charset=utf-8'
});
ctx.body = {
code: 200,
msg: '纯强缓存-Cache-Control,有效期60秒',
time: new Date().toLocaleString()
};
});
首次请求后,命中强制缓存(disk cache);60s后缓存过期,重新请求资源

Expires(HTTP/1.0 标准,已被Cache-Control替代):指定资源过期的GMT 绝对时间,依赖客户端本地时间,若客户端时间与服务端不一致会导致缓存失效,示例:Expires: Mon, 02 Feb 2026 10:00:00 GMT。
本地node搭建Koa测试强缓存
ts
// 2. 纯强缓存:Expires(HTTP/1.0,有效期60秒,对比测试)
router.get('/cache/strong/expires', async (ctx) => {
const expireTime = new Date(Date.now() + 60 * 1000).toGMTString(); // 60秒后过期(GMT时间)
ctx.set({
Expires: expireTime,
'Content-Type': 'application/json;charset=utf-8'
});
ctx.body = {
code: 200,
msg: '纯强缓存-Expires,有效期60秒',
time: new Date().toLocaleString()
};
});
协商缓存
强缓存过期后,浏览器首次请求某资源(GET/HEAD),服务端返回 200 OK 的同时,会在响应头中写入协商缓存标识:
- 写入Last-Modified: (GMT时间,资源最后修改的格林威治时间);
- 写入ETag: (唯一标识,如强 ETag"e1f90f89"、弱 ETagW/"e1f90f89",含双引号 / 前缀);
浏览器接收到响应后,会将这两个标识的值与对应的资源进行关联,一起存储到本地缓存(内存 / 磁盘)中,同时记录资源的 URL、强缓存规则等信息,形成「资源 - 缓存标识」的映射关系
当浏览器再次请求同一资源 时,且本地缓存中仍保留该资源的协商缓存标识 (未被手动清除 / 浏览器缓存淘汰),会自动携带协商缓存请求头
Last-Modified(响应头)→ If-Modified-Since(请求头)
- 服务端返回资源最后修改的 GMT 时间
- 浏览器后续请求携带该时间,服务端对比:若资源修改时间晚于该值,说明资源更新,返回 200;否则返回 304
⚠️ 精度仅秒级(毫秒修改无法识别)、文件内容未变但修改时间变(误判为更新)。
ts
// 工具函数:获取文件信息(修改时间、内容)
const getFileInfo = () => {
const stats = fs.statSync(staticImgPath); // 文件状态
const content = fs.readFileSync(staticImgPath); // 文件内容
return {
mtime: stats.mtime.toGMTString(), // 最后修改时间(GMT格式,适配Last-Modified)
content,
contentMd5: md5(content) // 内容MD5(作为强ETag)
};
};
// 3. 纯协商缓存:Last-Modified + If-Modified-Since
router.get('/cache/negotiate/last-modified', async (ctx) => {
const { mtime, content } = getFileInfo();
// 设置Last-Modified响应头
ctx.set({
'Last-Modified': mtime,
'Content-Type': 'image/jpeg'
});
// 获取请求头的If-Modified-Since
const ifModifiedSince = ctx.headers['if-modified-since'];
// 对比:时间一致则协商缓存命中,返回304
if (ifModifiedSince === mtime) {
ctx.status = 304; // 304无响应体
return;
}
// 未命中,返回200+资源
ctx.status = 200;
ctx.body = content;
});
ETag(响应头)→ If-None-Match(请求头):优先级更高
- 服务端根据资源内容生成唯一标识(如 MD5 哈希、文件大小 + 修改时间),分为强 ETag(内容完全一致才匹配,如"123456")和弱 ETag(内容核心一致即可,如W/"123456",前缀W/);
- 浏览器后续请求携带该标识,服务端对比:标识不一致则资源更新,返回 200;否则返回 304
ts
// 4. 纯协商缓存:ETag + If-None-Match(强ETag,优先级更高)
router.get('/cache/negotiate/etag', async (ctx) => {
const { content, contentMd5 } = getFileInfo();
const etag = `"${contentMd5}"`; // 强ETag,包裹双引号(HTTP标准)
// 设置ETag响应头
ctx.set({
ETag: etag,
'Content-Type': 'image/jpeg'
});
// 获取请求头的If-None-Match
const ifNoneMatch = ctx.headers['if-none-match'];
// 对比:标识一致则协商缓存命中,返回304
if (ifNoneMatch === etag) {
ctx.status = 304;
return;
}
// 未命中,返回200+资源
ctx.status = 200;
ctx.body = content;
});

生产环境推荐:强缓存+协商缓存(Cache-Control+ETag+Last-Modified)
ts
// 5. 生产环境推荐:强缓存+协商缓存(Cache-Control+ETag+Last-Modified)
router.get('/cache/combine', async (ctx) => {
const { mtime, content, contentMd5 } = getFileInfo();
const etag = `"${contentMd5}"`;
// 强缓存:120秒有效期;协商缓存:ETag+Last-Modified(ETag优先级更高)
ctx.set({
'Cache-Control': 'public, max-age=120',
ETag: etag,
'Last-Modified': mtime,
'Content-Type': 'image/jpeg'
});
// 先判断ETag,再判断Last-Modified
const ifNoneMatch = ctx.headers['if-none-match'];
const ifModifiedSince = ctx.headers['if-modified-since'];
if (ifNoneMatch === etag || ifModifiedSince === mtime) {
ctx.status = 304;
return;
}
ctx.status = 200;
ctx.body = content;
});
文件传输压缩
Gzip / Brotli
Gzip 是 HTTP 传输中服务端压缩、浏览器自动解压缩的高效数据压缩方案,核心作用是大幅减小传输内容的体积、降低网络带宽消耗、提升资源加载速度,尤其对文本类资源效果显著 ;二进制资源(图片 / 视频 / 音频 / 压缩包) 本身已做过专业压缩(如 JPG/PNG/MP4),Gzip 压缩几乎无效果
Brotli Gzip 的升级版,由 Google 开发,压缩率比 Gzip 高 10%-20%,且解压缩速度更快,现代浏览器(Chrome/Firefox/Edge/Safari11+)均支持。Koa 中可通过koa-brotli中间件开启,配置与 Gzip 类似,生产环境可同时开启 Gzip 和 Brotli,服务端会根据浏览器的Accept-Encoding自动选择最优压缩方式。
后端koa项目搭建: koa-compress 插件开启压缩
ts
const compress = require('koa-compress');
const app = new Koa();
const PORT = 3001;
// 配置Gzip压缩规则(核心:放在所有中间件最前面,保证所有响应都能被压缩)
app.use(compress({
// 压缩阈值:仅对大小≥1024字节的响应进行压缩
threshold: 1024,
// 压缩级别:6(1=最快,9=压缩率最高,默认6)
flush: 6,
// 限定压缩的资源类型:仅文本类,排除二进制(关键!避免对图片等无效压缩)
filter: (contentType) => {
return [
'text/html',
'text/css',
'text/plain',
'application/json',
'application/javascript',
'application/x-javascript',
'text/javascript'
].some(type => contentType.includes(type));
}
}));
注册接口
ts
// 验证Gzip压缩:大JSON文件
router.get('/cache/gzip/test-json', async (ctx) => {
const bigJsonPath = path.join(__dirname, 'public/big-data.json');
const bigJsonContent = fs.readFileSync(bigJsonPath, 'utf8');
const etag = `"${md5(bigJsonContent)}"`;
// 带缓存的Gzip测试(生产环境配置:强缓存+协商缓存+Gzip)
ctx.set({
'Cache-Control': 'public, max-age=3600',
'ETag': etag,
'Last-Modified': new Date(fs.statSync(bigJsonPath).mtime).toGMTString(),
'Content-Type': 'application/json;charset=utf-8'
});
if (ctx.headers['if-none-match'] === etag) {
ctx.status = 304;
return;
}
ctx.status = 200;
ctx.body = bigJsonContent;
});
压缩前:

压缩后:

在相应头中查看content-encoding 字段,可见请求开启了压缩,使用的是 Brotli 压缩

压缩最佳实践
-
仅对文本类资源开启 Gzip / Brotli
严格通过filter排除二进制资源(图片 / 视频 / 音频 /zip/rar 等),避免无意义的 CPU 消耗。
-
设置合理的压缩阈值
小文件(<2KB)无需压缩,因为压缩后的体积减少量远不足以抵消服务端的压缩 CPU 损耗和浏览器的解压缩损耗。
-
Gzip / Brotli 与 CDN 结合使用
实际生产中,不建议在应用层(Koa/Express)开启 Gzip,而是在反向代理层(Nginx) 或CDN开启 :CDN 节点离用户更近,压缩后的资源传输距离更短,速度更快;
-
Gzip 与静态资源打包结合
前端通过 Webpack/Vite 打包时,对 JS/CSS 进行代码压缩(Terser/Cssnano),再结合服务端 Gzip 压缩,双重压缩能进一步减小体积(代码压缩先剔除冗余代码,Gzip 再对纯文本压缩)。
HTTP 2.0
HTTP/2.0 相比之前的版本(如 HTTP/1.1)在性能、并发性、传输效率和安全性等方面均有显著提升
-
二进制协议
HTTP/2.0 采用二进制格式传输数据,替代了 HTTP/1.1 的文本格式。二进制协议减少了数据解析的复杂性,提高了传输效率,同时降低了出错率,使数据传输更可靠。
-
多路复用(Multiplexing)
HTTP/2.0 允许在单个 TCP 连接上并行发送多个请求和响应,实现了并发传输,显著提升了连接利用率和页面加载速度。
-
头部压缩(Header Compression)
HTTP/2.0 使用 HPACK 算法对请求和响应的头部信息进行压缩,减少了数据传输量
-
服务器推送(Server Push)
HTTP/2.0 允许服务器在客户端请求之前主动推送资源(如 CSS、JavaScript 文件),减少了客户端的等待时间
-
流量控制(Flow Control)
HTTP/2.0 引入了流量控制机制,通过流控制窗口和令牌管理数据传输速度,防止客户端或服务器因接收数据过快而无法处理
-
优先级设定(Priority Handling)
HTTP/2.0 允许为每个流分配优先级,确保关键资源(如主内容)优先传输,非关键资源(如图片)延迟处理
-
安全性增强
-
连接效率提升