在现代 Web 开发中,缓存机制是提升页面性能、减少服务器负载的关键技术。合理的缓存策略能将页面加载速度提升 50%以上,同时降低 40%-60% 的服务器请求压力。本文将系统讲解前端缓存的工作原理,并通过一个完整的实战项目,展示不同缓存策略的实现方式与应用场景。
一、前端缓存基础概念
前端缓存指的是浏览器或代理服务器对已请求资源的本地存储机制,其核心目标是避免重复请求相同资源。
根据缓存验证方式的不同,可分为三大类:
- 无缓存:强制每次请求都获取完整资源
- 强缓存:在有效期内直接使用本地缓存,不发起请求
- 协商缓存:通过服务器验证后决定是否使用缓存
二、实战项目搭建
我们将通过一个完整的 Node.js 项目,实际演示不同缓存策略的效果。
1. 项目结构
.
|-- package-lock.json # 依赖版本锁定文件
|-- package.json # 项目依赖配置
|-- public # 静态资源目录
| |-- images # 图片资源(用于缓存演示)
| | |-- 00.jpg # 无缓存示例图片
| | |-- 01.png # Expires强缓存示例
| | |-- 02.png # Cache-Control强缓存示例
| | |-- 03.png # Last-Modified协商缓存示例
| | `-- 04.png # ETag协商缓存示例
| `-- index.html # 主页面,展示所有示例图片
|-- src
| `-- app.ts # 服务器代码,实现不同缓存策略
`-- tsconfig.json # TypeScript配置文件
2. 环境配置
- 安装依赖
npm i -D @types/node nodemon ts-node typescript
@types/node
:Node.js 的 TypeScript 类型定义nodemon
:开发时自动重启服务器ts-node
:直接运行 TypeScript 代码typescript
:TypeScript 编译器
- TypeScript 配置(tsconfig.json)
{
"compilerOptions": {
"target": "ES2016", // 编译目标为ES2016
"module": "commonjs", // 模块系统使用commonjs
"esModuleInterop": true, // 允许ES模块与CommonJS互操作
"strict": true, // 启用严格类型检查
"skipLibCheck": true // 跳过库文件类型检查
},
"exclude": ["node_modules"] // 排除node_modules目录
}
- 创建演示页面(public/index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>前端缓存演示</title>
<style>
body {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h2 {
color: #2c3e50;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.example {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>前端缓存策略演示</h1>
<div class="example">
<h2>1. 无缓存策略</h2>
<p>每次刷新都会重新请求资源</p>
<img width="200" src="/images/00.jpg" alt="无缓存示例图" />
</div>
<div class="example">
<h2>2. 强缓存策略</h2>
<p>Expires(HTTP/1.0)- 3秒内不重新请求</p>
<img width="200" src="/images/01.png" alt="Expires缓存示例图" />
<p>Cache-Control(HTTP/1.1)- 5秒内不重新请求</p>
<img width="200" src="/images/02.png" alt="Cache-Control缓存示例图" />
</div>
<div class="example">
<h2>3. 协商缓存策略</h2>
<p>Last-Modified + If-Modified-Since - 基于修改时间验证</p>
<img width="200" src="/images/03.png" alt="Last-Modified缓存示例图" />
<p>ETag + If-None-Match - 基于内容哈希验证</p>
<img width="200" src="/images/04.png" alt="ETag缓存示例图" />
</div>
</body>
</html>
三、缓存策略详解与实现
接下来,我们将在src/app.ts
中实现不同的缓存策略,通过代码展示其工作原理。
1. 服务器基础配置
首先搭建基础的 HTTP 服务器框架:
import http from "http";
import path from "path";
import fs from "fs/promises";
import crypto from "crypto";
// 配置服务器端口和静态资源目录
const PORT = 3000;
const publicDir = path.join(__dirname, "../public");
// 创建HTTP服务器
const server = http.createServer(async (req, res) => {
try {
const pathname = req.url;
// 处理根路径请求,返回index.html
if (pathname === "/") {
const data = await fs.readFile(
path.join(publicDir, "index.html"),
"utf8"
);
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(data);
}
// 各种缓存策略的实现将在这里添加...
// 未匹配到任何路由
res.writeHead(404);
res.end("404 Not Found");
} catch (err: any) {
// 错误处理
if (err.code === "ENOENT") {
res.writeHead(404);
return res.end("File not found");
}
console.error("Server error:", err);
res.writeHead(500);
res.end("500 Internal Server Error");
}
});
// 启动服务器
server.listen(PORT, () => {
console.log(`服务器已启动,访问: http://localhost:${PORT}`);
});
2. 无缓存策略
无缓存策略强制浏览器每次都从服务器获取完整资源,不存储任何本地副本。
工作原理:
- 不设置缓存相关响应头,或显式声明禁止缓存
- 浏览器每次请求都会发送完整的 HTTP 请求
- 服务器始终返回 200 状态码和完整资源内容
无缓存请求流程图:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 发起请求 │
├────────────────>│
│ │
│ 返回200 + 资源 │
│<────────────────│
│ │
代码实现:
// 处理00.jpg - 无缓存示例
if (pathname === "/images/00.jpg") {
const imagePath = path.join(publicDir, "images/00.jpg");
const data = await fs.readFile(imagePath);
// 方式1:隐式无缓存(不设置缓存头)
// res.writeHead(200, { "Content-Type": "image/jpeg" });
// 方式2:显式声明不缓存(推荐生产环境使用)
res.writeHead(200, {
"Content-Type": "image/jpeg",
"Cache-Control": "no-store, no-cache", // 禁止缓存存储和使用
Pragma: "no-cache", // 兼容HTTP/1.0
Expires: "0", // 告知代理服务器不缓存
});
return res.end(data);
}
适用场景:
- 实时性要求极高的资源(如股票行情、实时监控数据)
- 支付信息、订单状态等敏感数据
- 频繁变动且必须显示最新版本的内容
3. 强缓存策略
强缓存允许浏览器在一定时间内直接使用本地缓存,不向服务器发起请求,是性能最优的缓存策略。
- Expires(
HTTP/1.0
)
Expires 是 HTTP/1.0
定义的强缓存头,通过设置绝对时间指定缓存过期时间。
工作原理:
- 服务器返回资源时,通过 Expires 头指定缓存过期的具体时间
- 浏览器将资源缓存至该时间点
- 过期前的请求直接使用本地缓存,不发送网络请求
强缓存(Expires)流程图:
首次请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 发起请求 │
├────────────────>│
│ │
│ 200 + 资源 + Expires │
│<────────────────│
│ │
有效期内再次请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 检查Expires │
│ 缓存未过期 │
│ │
│ 直接使用缓存 │
│ │
代码实现:
// 处理01.png - Expires强缓存示例
if (pathname === "/images/01.png") {
const imagePath = path.join(publicDir, "images/01.png");
const data = await fs.readFile(imagePath);
// 计算缓存过期时间:当前时间 + 3秒
const expiresTime = new Date(Date.now() + 3000).toUTCString();
res.writeHead(200, {
"Content-Type": "image/png",
Expires: expiresTime, // 格式必须为GMT时间:"Wed, 21 Oct 2023 07:28:00 GMT"
});
return res.end(data);
}
特点:
- 优点:实现简单,兼容性好
- 缺点:依赖客户端时钟准确性,时间精度为秒级
- 注意:在 HTTP/1.1 中,Expires 会被 Cache-Control 覆盖
- Cache-Control(HTTP/1.1)
Cache-Control 是 HTTP/1.1 推出的现代缓存头,通过相对时间(秒) 控制缓存,优先级高于 Expires。
工作原理:
- 使用 max-age 指定缓存有效时长(秒)
- 从浏览器接收响应时开始计时
- 有效期内直接使用本地缓存,不发起网络请求
Cache-Control 常用指令对比:
指令 | 含义描述 | 作用范围 | 核心特点 | 典型适用场景 |
---|---|---|---|---|
max-age=3600 | 缓存有效期为 3600 秒(1 小时) | 私有缓存(浏览器本地) | 过期前直接使用缓存,不发起请求 | 版本化静态资源(JS/CSS/ 图片) |
s-maxage=3600 | 共享缓存有效期为 3600 秒 | 共享缓存(CDN / 代理服务器) | 优先级高于 max-age,仅作用于共享缓存 | CDN 分发资源,平衡带宽与更新频率 |
public | 允许任何缓存(浏览器 / CDN 等)存储资源 | 所有类型缓存 | 打破默认的私有缓存限制 | 公共静态资源(首页图片、通用 JS) |
private | 仅限私有缓存存储,禁止共享缓存存储 | 仅私有缓存 | 保护用户个性化数据,避免跨用户泄露 | 用户个人中心、购物车等私有内容 |
immutable | 声明资源永不变更,忽略同 URL 的验证请求 | 私有缓存 | 阻止浏览器对未过期资源的冗余验证 | 带内容哈希的资源(如 style.5a7b.css) |
no-store | 禁止任何缓存存储资源 | 所有类型缓存 | 每次请求必须从服务器获取完整资源 | 支付页面、验证码、实时监控数据 |
no-cache | 允许缓存存储,但使用前必须服务器验证 | 所有类型缓存 | 触发协商缓存(需配合 ETag/Last-Modified) | 频繁更新的动态内容(新闻列表、商品详情) |
代码实现:
// 处理02.png - Cache-Control强缓存示例
if (pathname === "/images/02.png") {
const imagePath = path.join(publicDir, "images/02.png");
const data = await fs.readFile(imagePath);
res.writeHead(200, {
"Content-Type": "image/png",
"Cache-Control": "max-age=5", // 缓存5秒(从接收响应开始计算)
});
return res.end(data);
}
特点:
- 优点:时间计算精确,不依赖客户端时钟,控制粒度细
- 缺点:不兼容非常老旧的浏览器(如 IE6 及以下)
- 最佳实践:结合版本化资源(如
logo.v2.png
)使用长期缓存
4. 协商缓存策略
协商缓存通过服务器验证资源是否修改,决定是否复用缓存,既能保证资源新鲜度,又能减少不必要的传输。
- Last-Modified + If-Modified-Since
基于资源最后修改时间进行验证,适用于低频更新的资源。
工作原理:
- 首次请求:服务器返回资源最后修改时间(Last-Modified 头)
- 后续请求:浏览器携带该时间(If-Modified-Since 头)
- 服务器比对时间:
- 未修改 → 返回 304(无响应体),浏览器复用缓存
- 已修改 → 返回 200 及新资源,浏览器更新缓存
协商缓存(Last-Modified)流程图:
首次请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 发起请求 │
├────────────────>│
│ │
│200 + 资源 + Last-Modified│
│<────────────────│
│ │
后续请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│请求 + If-Modified-Since│
├────────────────>│
│ │
│ 比对时间 │
│ │
│ 未修改 │
│ 304响应 │
│<────────────────│
│ │
│ 使用缓存 │
│ │
代码实现:
// 处理03.png - Last-Modified协商缓存示例
if (pathname === "/images/03.png") {
const imagePath = path.join(publicDir, "images/03.png");
// 获取文件信息(包括修改时间)
const fileStats = await fs.stat(imagePath);
const fileData = await fs.readFile(imagePath);
// 格式化最后修改时间(必须为GMT格式)
const lastModified = fileStats.mtime.toUTCString();
// 获取浏览器发送的If-Modified-Since头
const ifModifiedSince = req.headers["if-modified-since"];
if (ifModifiedSince) {
// 转换为秒级时间戳进行比较(HTTP时间仅精确到秒)
const clientTime = Math.floor(new Date(ifModifiedSince).getTime() / 1000);
const serverTime = Math.floor(fileStats.mtime.getTime() / 1000);
// 如果客户端缓存时间 >= 服务器修改时间,说明资源未修改
if (clientTime >= serverTime) {
res.writeHead(304, {
"Last-Modified": lastModified, // 保持头信息一致
"Cache-Control": "no-cache", // 强制每次验证
});
return res.end(); // 304响应无响应体
}
}
// 资源已修改,返回新资源和新的Last-Modified
res.writeHead(200, {
"Content-Type": "image/png",
"Last-Modified": lastModified,
"Cache-Control": "no-cache", // 强制每次请求都进行验证
});
return res.end(fileData);
}
特点:
- 优点:实现简单,性能开销小
- 缺点:时间精度仅到秒级,无法识别 1 秒内的多次修改
- 注意:需确保服务器时间准确(建议使用 NTP 同步)
- ETag + If-None-Match
基于资源内容哈希进行验证,精度更高,适用于高频更新或时间戳不可靠的场景。
工作原理:
- 首次请求:服务器生成资源内容哈希(ETag 头)
- 后续请求:浏览器携带该哈希(If-None-Match 头)
- 服务器比对哈希:
- 未修改 → 返回 304,浏览器复用缓存
- 已修改 → 返回 200 及新资源,浏览器更新缓存
协商缓存(ETag)流程图:
首次请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 发起请求 │
├────────────────>│
│ │
│ 200 + 资源 + ETag │
│<────────────────│
│ │
后续请求:
┌─────────┐ ┌─────────┐
│ 浏览器 │ │ 服务器 │
└────┬────┘ └────┬────┘
│ │
│ 请求 + If-None-Match │
├────────────────>│
│ │
│ 比对哈希 │
│ │
│ 未修改 │
│ 304响应 │
│<────────────────│
│ │
│ 使用缓存 │
│ │
代码实现:
// 处理04.png - ETag协商缓存示例
if (pathname === "/images/04.png") {
const imagePath = path.join(publicDir, "images/04.png");
const fileData = await fs.readFile(imagePath);
// 生成资源内容哈希(ETag)
// 使用MD5算法生成16进制哈希值
const fileHash = crypto.createHash("md5").update(fileData).digest("hex");
// 强ETag必须包含在双引号中
const etag = `"${fileHash}"`;
// 获取浏览器发送的If-None-Match头
const ifNoneMatch = req.headers["if-none-match"];
// 比对ETag
if (ifNoneMatch === etag) {
// ETag匹配,资源未修改
res.writeHead(304, {
ETag: etag, // 保持ETag一致
"Cache-Control": "no-cache", // 强制每次验证
});
return res.end(); // 304响应无响应体
}
// 资源已修改,返回新资源和新ETag
res.writeHead(200, {
"Content-Type": "image/png",
ETag: etag,
"Cache-Control": "no-cache",
});
return res.end(fileData);
}
特点:
- 优点:精度极高(基于内容哈希),不受时钟影响
- 缺点:计算哈希有一定性能开销,集群环境需统一哈希算法
- 注意:ETag 分为强验证器(精确匹配)和弱验证器(前缀 W/,语义匹配)
四、缓存策略对比与最佳实践
不同缓存策略各有优劣,实际开发中需根据资源特性选择合适的方案:
缓存类型 | 核心优势 | 性能影响 | 适用场景 | 典型状态码 |
---|---|---|---|---|
无缓存 | 实时性 100% | 性能损耗高 | 实时数据、支付信息 | 200 |
强缓存(Expires) | 实现简单,兼容性好 | 性能最优 | 需兼容旧浏览器的静态资源 | 200(from cache) |
强缓存(Cache-Control) | 控制精确,现代标准 | 性能最优 | 版本化静态资源(JS/CSS/图片) | 200(from cache) |
协商缓存(Last-Modified) | 实现简单,开销小 | 中等性能 | 低频更新的文档、网页 | 304 |
协商缓存(ETag) | 精度高,可靠 | 中等性能 | 用户头像、高频更新资源 | 304 |
浏览器缓存状态标识说明:
- 200 OK:从服务器获取完整资源
- 200 OK (from cache):强缓存生效,使用本地缓存
- 304 Not Modified:协商缓存生效,使用本地缓存
- (from memory cache):使用内存缓存(临时存储)
- (from disk cache):使用磁盘缓存(持久存储)
最佳实践建议:
静态资源(JS/CSS/图片):
- 使用 Cache-Control 设置长期缓存(如 1 年)
- 配合资源版本化(如
app.8f3d.js
)确保更新 - 示例配置:
Cache-Control: public, max-age=31536000, immutable
API 数据:
- 对频繁变化数据使用协商缓存
- 对稳定数据结合强缓存和协商缓存
- 示例配置:
Cache-Control: no-cache
+ ETag/Last-Modified
调试技巧:
- 使用浏览器 Network 面板观察缓存状态
- 注意"Disable cache"选项对调试的影响
- 304 状态码表示协商缓存生效,200(from cache)表示强缓存生效
五、运行与验证
- 在
package.json
中添加启动脚本:
"scripts": {
"dev": "nodemon --exec ts-node src/app.ts"
}
- 启动服务器:
npm run dev
- 访问
http://localhost:3000
,打开浏览器开发者工具(F12)的 Network 面板:- 观察不同图片的请求状态和响应头
- 刷新页面,查看缓存策略的实际效果
- 修改图片内容后再次刷新,观察缓存更新情况
通过这个实战项目,我们系统学习了前端缓存的工作原理和实现方式。合理运用缓存策略,能显著提升 Web 应用性能,改善用户体验。在实际开发中,需根据具体业务场景,灵活组合不同的缓存策略,找到性能与新鲜度的最佳平衡点。