主题
09 · 缓存与压缩 · 让网站快 5 倍
生活类比:缓存像"茶水间的现成咖啡"——客户来了直接倒一杯,不用再去煮一壶。压缩像"真空包装食品"——同样的东西袋子小了,运起来快也省。
1. 为什么缓存 + 压缩这么重要
不优化 ─────► 1.5 MB JS 包 ─ 2 秒下载 ─ 用户看着 loading 转圈 ☹️
开 gzip ────► 400 KB 包 ─ 0.7 秒下载 ─
强缓存 ────► 0 KB(304) ─ 0 秒(命中本地)─ 🚀
proxy_cache ─► 后端不用算 ─ 0 后端 QPS ─ 💸收益:用户体验更好、服务器更省、带宽更便宜、SEO 评分更高。
2. 三层缓存,一图分清
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 浏览器缓存 │ ──►│ CDN 缓存 │ ──►│ Nginx 缓存 │ ──►│ 后端 / 文件│
│ (本地磁盘) │ │ (边缘节点) │ │ (proxy_cache) │ │ │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
▲ ▲ ▲
└─ Cache-Control / Expires 控制Nginx 主要做两件缓存:
- 响应头控制浏览器/CDN缓存(
Cache-Control/Expires) - 自己缓存上游响应(
proxy_cache)
3. 浏览器缓存 · expires + Cache-Control
3.1 强缓存 vs 协商缓存(必懂概念)
强缓存 → 命中:浏览器直接用本地,不发请求 (Cache-Control / Expires)
协商缓存 → 命中:发请求问服务器"变没变",没变就 304 (ETag / Last-Modified)3.2 配置示例
nginx
# 1. 带哈希的资源 → 强缓存 1 年(最优)
location ~* \.[a-f0-9]{8,}\.(?:js|css|png|jpg|webp|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 2. 普通图片 → 7 天
location ~* \.(jpg|png|gif|webp|svg|ico)$ {
expires 7d;
}
# 3. HTML → 不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
# 4. API 响应 → 不缓存
location /api/ {
add_header Cache-Control "no-store";
}3.3 ETag / Last-Modified
Nginx 默认会自动生成 ETag 和 Last-Modified(基于文件 inode + mtime + size)。
浏览器:GET /index.html
If-None-Match: "abc123"
If-Modified-Since: Wed, 01 Jan 2025 10:00:00 GMT
Nginx: 内容没变 → 304 Not Modified(不返响应体!)
内容变了 → 200 + 新内容4. gzip 压缩 · 让传输瘦 70%
nginx
http {
gzip on;
gzip_min_length 1k; # 小于 1KB 不压(不划算)
gzip_comp_level 5; # 1=快/差,9=慢/好;5 是甜点
gzip_vary on; # 加 Vary: Accept-Encoding 头
gzip_proxied any; # 也对反代响应压缩
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# ⚠️ 不要压 jpg/png/gif/zip/mp4 这种本身就压缩过的,越压越大且浪费 CPU
}| 文件类型 | 原始 | gzip 后 | 压缩比 |
|---|---|---|---|
| HTML | 100KB | 25KB | 75% |
| CSS | 80KB | 18KB | 78% |
| JS(min) | 1MB | 300KB | 70% |
| JSON API | 50KB | 8KB | 84% |
| 图片 | 500KB | 498KB | 0%(别压) |
5. brotli · 比 gzip 还小 20%
Google 的更新一代压缩算法。Nginx 需要装 ngx_brotli 模块(OpenResty / 多数发行版默认有)。
nginx
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;最佳实践:gzip + brotli 同时开启——浏览器支持哪个就用哪个(Accept-Encoding 自动协商)。
6. proxy_cache · 给后端响应加缓存
6.1 应用场景
后端有些接口结果几分钟才变一次(如商品详情、文章列表)——直接由 Nginx 缓存返回,几乎 0 后端压力。
6.2 完整配置
nginx
# ① 在 http 块里定义缓存区(可定义多个)
proxy_cache_path /var/cache/nginx/api
levels=1:2
keys_zone=api_cache:50m # 50MB 元数据 ≈ 40 万 key
max_size=2g # 磁盘上最多 2GB
inactive=60m # 60 分钟没人访问就清掉
use_temp_path=off;
# ② 在 location 里启用
location /api/products {
proxy_pass http://backend/products;
proxy_cache api_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m; # 200/302 缓存 10 分钟
proxy_cache_valid 404 1m; # 404 缓存 1 分钟
proxy_cache_valid any 30s; # 其他状态缓存 30 秒
proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
proxy_cache_lock on; # 同 key 并发只放一个回源
# 加一个响应头方便排查(命中/未命中)
add_header X-Cache-Status $upstream_cache_status;
}6.3 X-Cache-Status 5 种值
| 值 | 含义 |
|---|---|
MISS | 没缓存,刚去回源 |
HIT | 命中缓存 ✅ |
EXPIRED | 缓存过期,回源更新 |
BYPASS | 配置了绕过(proxy_cache_bypass) |
STALE | 上游挂了,返回过期缓存救命 |
调试小技巧:浏览器 DevTools → Network → 看
X-Cache-Status头,立刻知道有没有命中。
6.4 用 cookie / Authorization 头跳过缓存
nginx
proxy_cache_bypass $cookie_nocache $arg_nocache $http_authorization;
proxy_no_cache $cookie_nocache $arg_nocache $http_authorization;含义:登录用户(带 Authorization 头)不命中缓存,避免串号风险。
7. 直接返回 · expires 配 etag 配 gzip_static
如果你的 JS 已经在构建时预压缩过(生成 app.js.gz),可以让 Nginx 直接发预压缩文件:
nginx
gzip_static on; # 优先发 app.js.gz(如果存在)好处:CPU 不用每请求一次都压缩一次,瞬间提升 QPS。Vite / Webpack 都有 vite-plugin-compression / compression-webpack-plugin 能预压缩。
8. 实战:3 种典型场景的最佳配置
8.1 SPA 前端项目(最常见)
nginx
server {
listen 443 ssl http2;
root /var/www/dist;
index index.html;
# 预压缩
gzip_static on;
brotli_static on;
# 带 hash 的资源 → 1 年强缓存
location ~* \.[a-f0-9]{8,}\.(js|css|png|jpg|webp|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML → 不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
location / {
try_files $uri $uri/ /index.html;
}
}8.2 接口聚合 / API 网关
nginx
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api:50m max_size=1g inactive=10m;
server {
listen 443 ssl http2;
location /api/ {
proxy_pass http://api_backend/;
# 只缓存 GET
proxy_cache_methods GET HEAD;
proxy_cache api;
proxy_cache_valid 200 5m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status always;
}
}8.3 静态资源 CDN 源站
nginx
server {
listen 443 ssl http2;
server_name cdn.example.com;
root /var/cdn;
# CDN 友好头
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Access-Control-Allow-Origin "*" always;
# 跨域字体
location ~* \.(woff2?|ttf|eot)$ {
add_header Access-Control-Allow-Origin "*" always;
expires 1y;
}
gzip_static on;
}9. ⚠️ 缓存压缩 8 大坑
- HTML 被强缓存 → 用户死活刷不出新版本,永远记得 HTML
no-cache - gzip 在 CDN 后面失效 → CDN 回源时不带
Accept-Encoding,Nginx 没压。改gzip_proxied any - proxy_cache 缓存了登录态 → 串号事故;用
proxy_cache_bypass排除带 Authorization 的请求 - 缓存目录满磁盘 → 设
max_size、inactive,并开启监控 - Vary 头丢了 → CDN 给不同 Accept-Encoding 的用户返同一份;加
gzip_vary on - POST 请求被缓存 → Nginx 默认不缓存 POST,但确认下
proxy_cache_methods没乱配 - 预压缩文件没生效 →
gzip_static on但磁盘上没.gz文件 - 小文件越压越大 →
gzip_min_length至少 1k
10. 章末面试题速览
详见
qa.md第 26-28 题。
- 强缓存 vs 协商缓存? → 强缓存浏览器不发请求;协商缓存发请求看 304。
- 怎么让 CDN 知道资源该缓存多久? → 设
Cache-Control: public, max-age=N, s-maxage=N。 - proxy_cache 命中后用户每次都拿到同一个版本,怎么"灰度"? → 在
proxy_cache_key里加上$http_x_user_group之类的维度。
11. 一句话总结
缓存压缩 = "瘦身(gzip/brotli)" + "免出门(强缓存)" + "代购(proxy_cache)":上一套之后,你的网站速度通常能从 3 秒掉到 0.5 秒。
下一章 → 10 · 实战:SPA + API 网关:把前 9 章的知识捏成一份生产配置。
🎬 可视化演示
下方 demo 模拟一次 GET /api/products 请求,让你看 首次回源 (MISS) → 第二次命中缓存 (HIT) → 过期回源 (EXPIRED) 三种状态——直观感受 proxy_cache 怎么工作。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗