浏览器缓存

HTTP缓存机制

核心概念(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) => {
// 设置过期时间为1小时后
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', // 1年
immutable: true
}));

// HTML不缓存
app.get('/', (req, res) => {
res.setHeader('Cache-Control', 'no-cache');
res.sendFile(path.join(__dirname, 'index.html'));
});

// API不缓存
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

原理:

  1. 服务器返回资源时,带上Last-Modified(最后修改时间)
  2. 浏览器再次请求时,带上If-Modified-Since(上次的Last-Modified)
  3. 服务器对比时间,判断资源是否修改
  4. 未修改返回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();

// 获取客户端发送的If-Modified-Since
const ifModifiedSince = req.headers['if-modified-since'];

// 对比时间
if (ifModifiedSince === lastModified) {
// 未修改,返回304
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(推荐)

原理:

  1. 服务器返回资源时,带上ETag(资源唯一标识,通常是hash值)
  2. 浏览器再次请求时,带上If-None-Match(上次的ETag)
  3. 服务器对比ETag,判断资源是否修改
  4. 未修改返回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
// 1. 文件内容hash(推荐)
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}"`;
}

// 2. 文件大小 + 修改时间
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);

// 生成ETag
const hash = crypto.createHash('md5').update(content).digest('hex');
const etag = `"${hash}"`;

// 获取客户端发送的If-None-Match
const ifNoneMatch = req.headers['if-none-match'];

// 对比ETag
if (ifNoneMatch === etag) {
// 未修改,返回304
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
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js', // JS文件hash
chunkFilename: '[name].[contenthash:8].js'
},

plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css' // CSS文件hash
})
],

module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
type: 'asset',
generator: {
filename: 'images/[name].[hash:8][ext]' // 图片hash
}
}
]
}
};

为什么使用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;

# HTML文件不缓存
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}

# 静态资源(带hash)永久缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# API接口不缓存
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,推荐使用
  • 流程:
    1. 首次请求:服务器返回资源 + ETag
    2. 再次请求:浏览器带上If-None-Match
    3. 服务器对比:未修改返回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
<!-- 不带hash -->
<script src="/js/app.js"></script>
<!-- 更新后需要清除缓存或加版本号 -->
<script src="/js/app.js?v=1.0.1"></script>

<!-- 带hash(推荐) -->
<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
// 1. 添加时间戳
fetch('/api/data?t=' + Date.now());

// 2. 添加随机数
fetch('/api/data?r=' + Math.random());

// 3. 修改文件名(推荐)
// app.js → app.v2.js
// 或使用hash:app.a7b8c9d0.js

// 4. 设置请求头
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 {
# HTML不缓存
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";
}

# API不缓存
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
// Express实现ETag协商缓存
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const app = express();

// 生成ETag
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');

// 生成ETag
const etag = `"${generateETag(content)}"`;

// 获取客户端ETag
const clientETag = req.headers['if-none-match'];

// 对比ETag
if (clientETag === etag) {
// 未修改,返回304
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
// webpack.config.js
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'),
// JS文件使用contenthash
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: {
// 图片使用hash
filename: 'images/[name].[hash:8][ext]'
}
}
]
},

plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
// CSS文件使用contenthash
filename: 'css/[name].[contenthash:8].css'
})
]
};

// 构建后的文件名:
// js/main.a7b8c9d0.js
// css/main.e1f2g3h4.css
// images/logo.i5j6k7l8.png

案例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
// server.js - 完整的缓存策略实现
const express = require('express');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const app = express();

// 1. 静态资源(带hash)- 永久缓存
app.use('/static', express.static('public/static', {
maxAge: '1y',
immutable: true,
setHeaders: (res, filePath) => {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
}
}));

// 2. 图片资源 - 长期缓存
app.use('/images', express.static('public/images', {
maxAge: '30d',
setHeaders: (res, filePath) => {
res.setHeader('Cache-Control', 'public, max-age=2592000');
}
}));

// 3. HTML文件 - 协商缓存
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);
}
});

// 4. API接口 - 不缓存
app.get('/api/data', (req, res) => {
res.setHeader('Cache-Control', 'no-store');
res.json({ data: 'real-time data' });
});

// 5. 用户数据 - 私有缓存
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
# nginx.conf - Nginx缓存配置
server {
listen 80;
server_name example.com;
root /var/www/html;

# 1. HTML文件 - 协商缓存
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}

# 2. 静态资源(带hash)- 永久缓存
location ~* \.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# 3. 图片资源 - 长期缓存
location ~* \.(png|jpg|jpeg|gif|svg|ico|webp)$ {
add_header Cache-Control "public, max-age=2592000";
}

# 4. 字体文件 - 永久缓存
location ~* \.(woff|woff2|ttf|eot)$ {
add_header Cache-Control "public, max-age=31536000";
add_header Access-Control-Allow-Origin "*";
}

# 5. API接口 - 不缓存
location /api/ {
add_header Cache-Control "no-store";
proxy_pass http://backend:8080;
}

# 6. 开启Gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;
}