浏览器缓存
发表于更新于
常见们浏览器缓存
OHNIIHTTP缓存机制
核心概念(2大类型)
记忆口诀:强缓存不请求,协商缓存问一问
1 2
| 1. 强缓存:直接使用本地缓存,不请求服务器 2. 协商缓存:询问服务器缓存是否可用
|
缓存流程图:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 请求资源 ↓ 检查强缓存 ↓ 命中? → 是 → 直接使用缓存(200 from cache) ↓ 否 发送请求到服务器 ↓ 检查协商缓存 ↓ 未修改? → 是 → 使用缓存(304 Not Modified) ↓ 否 返回新资源(200 OK)
|
1. 强缓存(Strong Cache)
1.1 Expires(HTTP/1.0)
原理:
- 服务器返回资源的过期时间(绝对时间)
- 浏览器判断当前时间是否超过过期时间
- 未过期则直接使用缓存
响应头示例:
1
| Expires: Wed, 21 Oct 2026 07:28:00 GMT
|
缺点:
1 2 3
| 1. 依赖客户端时间,如果客户端时间不准确会导致缓存失效 2. 服务器时间和客户端时间可能不一致 3. 已被Cache-Control替代
|
Node.js示例:
1 2 3 4 5 6 7 8 9
| const express = require('express'); const app = express();
app.get('/api/data', (req, res) => { const expires = new Date(Date.now() + 3600000); res.setHeader('Expires', expires.toUTCString()); res.json({ data: 'some data' }); });
|
1.2 Cache-Control(HTTP/1.1,推荐)
原理:
- 使用相对时间(秒数)
- 优先级高于Expires
- 更灵活、更精确
常用指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| # 1. max-age:缓存时间(秒) Cache-Control: max-age=3600 # 缓存1小时
# 2. no-cache:不使用强缓存,使用协商缓存 Cache-Control: no-cache
# 3. no-store:不缓存任何内容 Cache-Control: no-store
# 4. public:可被任何缓存(浏览器、CDN) Cache-Control: public, max-age=31536000
# 5. private:只能被浏览器缓存,不能被CDN缓存 Cache-Control: private, max-age=3600
# 6. must-revalidate:缓存过期后必须验证 Cache-Control: max-age=3600, must-revalidate
# 7. immutable:资源永不改变(适合带hash的文件) Cache-Control: max-age=31536000, immutable
|
组合使用:
1 2 3 4 5 6 7 8 9 10 11
| # 静态资源(带hash):永久缓存 Cache-Control: public, max-age=31536000, immutable
# HTML文件:不缓存或协商缓存 Cache-Control: no-cache
# API接口:不缓存 Cache-Control: no-store
# 用户私有数据:浏览器缓存,CDN不缓存 Cache-Control: private, max-age=3600
|
Node.js示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const express = require('express'); const path = require('path'); const app = express();
app.use('/static', express.static('public', { maxAge: '1y', immutable: true }));
app.get('/', (req, res) => { res.setHeader('Cache-Control', 'no-cache'); res.sendFile(path.join(__dirname, 'index.html')); });
app.get('/api/data', (req, res) => { res.setHeader('Cache-Control', 'no-store'); res.json({ data: 'some data' }); });
|
2. 协商缓存(Negotiation Cache)
2.1 Last-Modified / If-Modified-Since
原理:
- 服务器返回资源时,带上Last-Modified(最后修改时间)
- 浏览器再次请求时,带上If-Modified-Since(上次的Last-Modified)
- 服务器对比时间,判断资源是否修改
- 未修改返回304,已修改返回200和新资源
流程:
1 2 3 4 5 6 7 8 9
| 第一次请求: 客户端 → 服务器 服务器 → 客户端(200 + Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT)
第二次请求: 客户端 → 服务器(If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT) 服务器判断: - 未修改 → 304 Not Modified - 已修改 → 200 OK + 新资源 + 新的Last-Modified
|
响应头示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| # 第一次响应 HTTP/1.1 200 OK Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT Cache-Control: no-cache
GET /api/data HTTP/1.1 If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT
HTTP/1.1 304 Not Modified
HTTP/1.1 200 OK Last-Modified: Wed, 21 Oct 2026 08:30:00 GMT
|
Node.js示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const express = require('express'); const fs = require('fs'); const app = express();
app.get('/api/data', (req, res) => { const filePath = './data.json'; const stats = fs.statSync(filePath); const lastModified = stats.mtime.toUTCString(); const ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince === lastModified) { res.status(304).end(); } else { res.setHeader('Last-Modified', lastModified); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(filePath); } });
|
缺点:
1 2 3 4
| 1. 只能精确到秒,1秒内多次修改无法识别 2. 某些服务器无法精确获取文件修改时间 3. 文件修改时间变了,但内容没变,也会重新请求 4. 已被ETag替代
|
2.2 ETag / If-None-Match(推荐)
原理:
- 服务器返回资源时,带上ETag(资源唯一标识,通常是hash值)
- 浏览器再次请求时,带上If-None-Match(上次的ETag)
- 服务器对比ETag,判断资源是否修改
- 未修改返回304,已修改返回200和新资源
流程:
1 2 3 4 5 6 7 8 9
| 第一次请求: 客户端 → 服务器 服务器 → 客户端(200 + ETag: "33a64df551425fcc55e")
第二次请求: 客户端 → 服务器(If-None-Match: "33a64df551425fcc55e") 服务器判断: - ETag匹配 → 304 Not Modified - ETag不匹配 → 200 OK + 新资源 + 新的ETag
|
响应头示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| # 第一次响应 HTTP/1.1 200 OK ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" Cache-Control: no-cache
GET /api/data HTTP/1.1 If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
HTTP/1.1 304 Not Modified
HTTP/1.1 200 OK ETag: "a7b8c9d0e1f2g3h4i5j6k7l8m9n0o1p2q3r4s5t6"
|
ETag生成方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const crypto = require('crypto'); const fs = require('fs');
function generateETag(filePath) { const content = fs.readFileSync(filePath); const hash = crypto.createHash('md5').update(content).digest('hex'); return `"${hash}"`; }
function generateETag(filePath) { const stats = fs.statSync(filePath); return `"${stats.size}-${stats.mtime.getTime()}"`; }
|
Node.js示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const express = require('express'); const crypto = require('crypto'); const fs = require('fs'); const app = express();
app.get('/api/data', (req, res) => { const filePath = './data.json'; const content = fs.readFileSync(filePath); const hash = crypto.createHash('md5').update(content).digest('hex'); const etag = `"${hash}"`; const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch === etag) { res.status(304).end(); } else { res.setHeader('ETag', etag); res.setHeader('Cache-Control', 'no-cache'); res.json(JSON.parse(content)); } });
|
优点:
1 2 3 4
| 1. 精确度高,内容不变ETag就不变 2. 不依赖时间,避免时间不准确问题 3. 可以识别1秒内的多次修改 4. 优先级高于Last-Modified
|
3. 缓存优先级
优先级顺序:
1 2 3
| 1. Cache-Control > Expires(强缓存) 2. ETag > Last-Modified(协商缓存) 3. 强缓存 > 协商缓存
|
完整流程:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 1. 检查Cache-Control/Expires ↓ 未过期 直接使用缓存(200 from disk cache / memory cache) ↓ 已过期 2. 发送请求,带上If-None-Match/If-Modified-Since 3. 服务器检查ETag/Last-Modified ↓ 未修改 返回304 Not Modified ↓ 已修改 返回200 OK + 新资源
|
4. 缓存策略最佳实践
4.1 不同资源的缓存策略
HTML文件
1 2 3 4 5
| # 不缓存或使用协商缓存 Cache-Control: no-cache # 或 Cache-Control: no-cache ETag: "xxx"
|
CSS/JS文件(带hash)
1 2
| # 永久缓存 Cache-Control: public, max-age=31536000, immutable
|
CSS/JS文件(不带hash)
1 2 3
| # 短期缓存 + 协商缓存 Cache-Control: public, max-age=3600 ETag: "xxx"
|
图片资源
1 2
| # 长期缓存 Cache-Control: public, max-age=2592000 # 30天
|
API接口
1 2 3 4
| # 不缓存 Cache-Control: no-store # 或短期缓存 Cache-Control: private, max-age=60
|
用户私有数据
1 2
| # 浏览器缓存,CDN不缓存 Cache-Control: private, max-age=3600
|
4.2 Webpack配置文件hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| module.exports = { output: { filename: '[name].[contenthash:8].js', chunkFilename: '[name].[contenthash:8].js' }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' }) ], module: { rules: [ { test: /\.(png|jpg|gif)$/, type: 'asset', generator: { filename: 'images/[name].[hash:8][ext]' } } ] } };
|
为什么使用contenthash?
1 2 3
| 1. 文件内容变化,hash才变化 2. 充分利用浏览器缓存 3. 只更新修改的文件,其他文件继续使用缓存
|
4.3 Nginx缓存配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| server { listen 80; server_name example.com; location / { add_header Cache-Control "no-cache"; try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } location /api/ { add_header Cache-Control "no-store"; proxy_pass http://backend; } }
|
5. 面试口述版本
面试官:请说说强缓存和协商缓存的区别
回答框架:
“HTTP缓存分为强缓存和协商缓存两种:
1. 强缓存(不请求服务器)
- 原理:浏览器直接使用本地缓存,不发送请求到服务器
- 响应状态:200 (from disk cache / memory cache)
- 实现方式:
- Expires:HTTP/1.0,绝对时间,已过时
- Cache-Control:HTTP/1.1,相对时间,推荐使用
- 常用指令:
- max-age=3600:缓存1小时
- public:可被任何缓存
- private:只能被浏览器缓存
- no-cache:不使用强缓存,使用协商缓存
- no-store:不缓存
2. 协商缓存(询问服务器)
- 原理:浏览器发送请求到服务器,询问缓存是否可用
- 响应状态:304 Not Modified(未修改)或 200 OK(已修改)
- 实现方式:
- Last-Modified / If-Modified-Since:基于修改时间
- ETag / If-None-Match:基于内容hash,推荐使用
- 流程:
- 首次请求:服务器返回资源 + ETag
- 再次请求:浏览器带上If-None-Match
- 服务器对比:未修改返回304,已修改返回200
3. 区别对比
| 特性 |
强缓存 |
协商缓存 |
| 是否请求服务器 |
否 |
是 |
| 响应状态码 |
200 (from cache) |
304 / 200 |
| 性能 |
最快 |
较快 |
| 实现 |
Cache-Control |
ETag |
4. 缓存策略
- HTML:no-cache(协商缓存)
- CSS/JS(带hash):max-age=31536000(永久缓存)
- 图片:max-age=2592000(30天)
- API:no-store(不缓存)
5. 优先级
- Cache-Control > Expires
- ETag > Last-Modified
- 强缓存 > 协商缓存
实际开发中,我会根据资源类型选择合适的缓存策略,静态资源使用强缓存,HTML使用协商缓存,充分利用浏览器缓存提升性能。”
6. 高频追问
Q1: 强缓存和协商缓存的执行流程?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 完整流程:
1. 浏览器发起请求 ↓ 2. 检查是否有缓存 ↓ 无缓存 请求服务器 → 返回资源 + 缓存标识 ↓ 有缓存 3. 检查强缓存(Cache-Control/Expires) ↓ 未过期 直接使用缓存(200 from cache) ↓ 已过期 4. 发送请求到服务器(带上If-None-Match/If-Modified-Since) ↓ 5. 服务器检查协商缓存(ETag/Last-Modified) ↓ 未修改 返回304 Not Modified → 使用缓存 ↓ 已修改 返回200 OK + 新资源 + 新的缓存标识
|
Q2: Cache-Control的常用指令有哪些?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 缓存时间 max-age=3600 # 缓存3600秒
# 缓存范围 public # 可被任何缓存(浏览器、CDN、代理) private # 只能被浏览器缓存
# 缓存策略 no-cache # 不使用强缓存,使用协商缓存 no-store # 不缓存任何内容 must-revalidate # 缓存过期后必须验证 immutable # 资源永不改变(适合带hash的文件)
# 组合使用 Cache-Control: public, max-age=31536000, immutable Cache-Control: private, max-age=3600, must-revalidate
|
Q3: ETag和Last-Modified的区别?
| 特性 |
ETag |
Last-Modified |
| 基于 |
内容hash |
修改时间 |
| 精确度 |
高(内容级别) |
低(秒级别) |
| 性能 |
需要计算hash |
直接读取时间 |
| 优先级 |
高 |
低 |
| 适用场景 |
内容频繁变化 |
内容不常变化 |
ETag优势:
- 精确到内容,内容不变ETag就不变
- 可以识别1秒内的多次修改
- 不依赖时间,避免时间不准确问题
Last-Modified缺点:
- 只能精确到秒
- 文件修改时间变了,但内容没变,也会重新请求
- 依赖服务器时间
Q4: 为什么静态资源要带hash?
原因:
1 2 3 4 5 6 7 8 9 10 11 12
| 1. 充分利用强缓存 - 带hash的文件可以设置永久缓存(max-age=31536000) - 文件内容变化,hash就变化,浏览器会请求新文件 - 文件内容不变,hash不变,浏览器继续使用缓存
2. 避免缓存问题 - 不带hash:文件更新后,浏览器可能还在使用旧缓存 - 带hash:文件更新后,hash变化,浏览器自动请求新文件
3. 提升性能 - 只更新修改的文件,其他文件继续使用缓存 - 减少不必要的请求
|
示例:
1 2 3 4 5 6 7 8 9
| <script src="/js/app.js"></script>
<script src="/js/app.js?v=1.0.1"></script>
<script src="/js/app.a7b8c9d0.js"></script>
<script src="/js/app.e1f2g3h4.js"></script>
|
Q5: 如何强制刷新缓存?
用户操作:
1 2 3 4 5 6 7 8 9 10
| 1. 普通刷新(F5) - 跳过强缓存 - 使用协商缓存
2. 强制刷新(Ctrl+F5 / Cmd+Shift+R) - 跳过所有缓存 - 直接请求服务器
3. 地址栏回车 - 使用强缓存和协商缓存
|
开发者操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| fetch('/api/data?t=' + Date.now());
fetch('/api/data?r=' + Math.random());
fetch('/api/data', { headers: { 'Cache-Control': 'no-cache' } });
|
服务器操作:
1 2 3 4 5
| # 设置不缓存 Cache-Control: no-store
# 或强制验证 Cache-Control: no-cache, must-revalidate
|
Q6: from disk cache 和 from memory cache 的区别?
from memory cache(内存缓存)
1 2 3 4 5 6 7 8 9
| 特点: - 存储在内存中 - 读取速度最快 - 关闭标签页后失效 - 容量小
适用: - 当前页面使用的资源 - 小文件(图片、CSS、JS)
|
from disk cache(磁盘缓存)
1 2 3 4 5 6 7 8 9
| 特点: - 存储在硬盘中 - 读取速度较快 - 关闭浏览器后仍存在 - 容量大
适用: - 大文件 - 长期缓存的资源
|
浏览器缓存策略:
1 2 3 4 5 6
| 1. 首次加载:请求服务器 2. 再次访问: - 小文件 → memory cache - 大文件 → disk cache 3. 关闭标签页:memory cache清空 4. 再次打开:disk cache
|
Q7: 如何设计一个完整的缓存策略?
1. HTML文件
1 2 3 4 5 6 7
| # 不缓存或协商缓存 Cache-Control: no-cache ETag: "xxx"
原因: - HTML是入口文件,需要及时更新 - 使用协商缓存,服务器可以控制是否更新
|
2. CSS/JS文件(带hash)
1 2 3 4 5 6 7
| # 永久缓存 Cache-Control: public, max-age=31536000, immutable
原因: - 文件名带hash,内容变化hash就变化 - 可以安全地永久缓存 - immutable告诉浏览器资源永不改变
|
3. 图片资源
1 2 3 4 5 6
| # 长期缓存 Cache-Control: public, max-age=2592000 # 30天
原因: - 图片不常变化 - 可以设置较长的缓存时间
|
4. API接口
1 2 3 4 5 6 7 8 9
| # 不缓存 Cache-Control: no-store
# 或短期缓存 Cache-Control: private, max-age=60
原因: - 数据实时性要求高 - 不缓存或短期缓存
|
5. 第三方库(CDN)
1 2 3 4 5 6
| # 永久缓存 Cache-Control: public, max-age=31536000
原因: - 版本固定,不会变化 - 可以永久缓存
|
完整配置示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| server { location / { add_header Cache-Control "no-cache"; } location ~* \.(js|css)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } location ~* \.(png|jpg|jpeg|gif|svg)$ { add_header Cache-Control "public, max-age=2592000"; } location /api/ { add_header Cache-Control "no-store"; } }
|
7. 实战案例
案例1:实现协商缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| const express = require('express'); const crypto = require('crypto'); const fs = require('fs'); const app = express();
function generateETag(content) { return crypto .createHash('md5') .update(content) .digest('hex'); }
app.get('/api/data', (req, res) => { const filePath = './data.json'; const content = fs.readFileSync(filePath, 'utf-8'); const etag = `"${generateETag(content)}"`; const clientETag = req.headers['if-none-match']; if (clientETag === etag) { console.log('缓存命中,返回304'); res.status(304).end(); } else { console.log('缓存未命中,返回200'); res.setHeader('ETag', etag); res.setHeader('Cache-Control', 'no-cache'); res.json(JSON.parse(content)); } });
app.listen(3000, () => { console.log('Server running on port 3000'); });
|
案例2:Webpack配置文件hash
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = { mode: 'production', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'js/[name].[contenthash:8].js', chunkFilename: 'js/[name].[contenthash:8].js', clean: true }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] }, { test: /\.(png|jpg|gif|svg)$/, type: 'asset', generator: { filename: 'images/[name].[hash:8][ext]' } } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }) ] };
|
案例3:完整的缓存策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| const express = require('express'); const path = require('path'); const crypto = require('crypto'); const fs = require('fs'); const app = express();
app.use('/static', express.static('public/static', { maxAge: '1y', immutable: true, setHeaders: (res, filePath) => { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } }));
app.use('/images', express.static('public/images', { maxAge: '30d', setHeaders: (res, filePath) => { res.setHeader('Cache-Control', 'public, max-age=2592000'); } }));
app.get('/', (req, res) => { const filePath = path.join(__dirname, 'public/index.html'); const content = fs.readFileSync(filePath); const etag = `"${crypto.createHash('md5').update(content).digest('hex')}"`; if (req.headers['if-none-match'] === etag) { res.status(304).end(); } else { res.setHeader('ETag', etag); res.setHeader('Cache-Control', 'no-cache'); res.sendFile(filePath); } });
app.get('/api/data', (req, res) => { res.setHeader('Cache-Control', 'no-store'); res.json({ data: 'real-time data' }); });
app.get('/api/user', (req, res) => { res.setHeader('Cache-Control', 'private, max-age=3600'); res.json({ user: 'user data' }); });
app.listen(3000, () => { console.log('Server running on port 3000'); });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| server { listen 80; server_name example.com; root /var/www/html; location / { add_header Cache-Control "no-cache"; try_files $uri $uri/ /index.html; } location ~* \.(js|css)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ { add_header Cache-Control "public, max-age=2592000"; } location ~* \.(woff|woff2|ttf|eot)$ { add_header Cache-Control "public, max-age=31536000"; add_header Access-Control-Allow-Origin "*"; } location /api/ { add_header Cache-Control "no-store"; proxy_pass http://backend:8080; } gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1000; }
|