Skip to content

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 主要做两件缓存

  1. 响应头控制浏览器/CDN缓存Cache-Control / Expires
  2. 自己缓存上游响应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 后压缩比
HTML100KB25KB75%
CSS80KB18KB78%
JS(min)1MB300KB70%
JSON API50KB8KB84%
图片500KB498KB0%(别压)

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 头,立刻知道有没有命中。

nginx
proxy_cache_bypass $cookie_nocache $arg_nocache $http_authorization;
proxy_no_cache     $cookie_nocache $arg_nocache $http_authorization;

含义:登录用户(带 Authorization 头)不命中缓存,避免串号风险。


7. 直接返回 · expiresetaggzip_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 大坑

  1. HTML 被强缓存 → 用户死活刷不出新版本,永远记得 HTML no-cache
  2. gzip 在 CDN 后面失效 → CDN 回源时不带 Accept-Encoding,Nginx 没压。改 gzip_proxied any
  3. proxy_cache 缓存了登录态 → 串号事故;用 proxy_cache_bypass 排除带 Authorization 的请求
  4. 缓存目录满磁盘 → 设 max_sizeinactive,并开启监控
  5. Vary 头丢了 → CDN 给不同 Accept-Encoding 的用户返同一份;加 gzip_vary on
  6. POST 请求被缓存 → Nginx 默认不缓存 POST,但确认下 proxy_cache_methods 没乱配
  7. 预压缩文件没生效gzip_static on 但磁盘上没 .gz 文件
  8. 小文件越压越大gzip_min_length 至少 1k

10. 章末面试题速览

详见 qa.md 第 26-28 题。

  1. 强缓存 vs 协商缓存? → 强缓存浏览器不发请求;协商缓存发请求看 304。
  2. 怎么让 CDN 知道资源该缓存多久? → 设 Cache-Control: public, max-age=N, s-maxage=N
  3. 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 怎么工作。

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗