Nginx浏览器强制缓存

一、引言:为什么强制缓存是Web性能的"第一优先级"?

在所有Web性能优化手段中,浏览器强制缓存是唯一能将网络请求彻底归零的机制。当资源命中强制缓存时,浏览器直接从本地磁盘或内存读取,不发起DNS解析、TCP握手、TLS协商,甚至不会向Nginx发送任何HTTP请求。这意味着:

  • 用户感知加载时间为零;
  • 服务器带宽和CPU消耗为零;
  • CDN回源压力为零。

然而在实际项目中,强制缓存的配置往往走向两个极端:要么完全不配,每次刷新都全量下载;要么一刀切设置 expires 7d,导致发版后用户看不到更新,只能引导用户"清除浏览器缓存"。这两种做法的根源都是同一个:没有理解强制缓存的生效条件,也没有将缓存策略与前端工程化的内容哈希体系对齐

本文将从HTTP协议原理出发,结合Nginx配置与现代前端构建工具,帮你建立一套"该永久缓存的绝不验证、该更新的立即生效"的精准强制缓存体系。


二、核心原理:强制缓存的触发条件与决策流程

2.1 什么是强制缓存?

强制缓存(Strong Caching)是指浏览器在收到带有明确有效期标识的响应后,在有效期内完全跳过网络请求 ,直接使用本地副本的机制。它与协商缓存(304验证)的本质区别在于:强制缓存连条件请求都不发

2.2 触发条件

浏览器进入强制缓存路径需要同时满足以下条件:

  1. 响应包含有效的 Cache-Control: max-age=NExpires 头;
  2. 当前时间距响应生成时间未超过max-age(或未超过Expires指定的绝对时间);
  3. 用户未执行强制刷新(Ctrl+F5 / Cmd+Shift+R);
  4. 响应未被标记为 no-store

2.3 决策流程图

复制代码
浏览器发起资源请求
        │
        ▼
┌───────────────────────────┐
│ 检查 Cache-Control       │
│ max-age 是否过期?        │
└────────────┬──────────────┘
      未过期  │         已过期/无指令
             ▼                  ▼
    ┌────────────────┐   进入协商缓存流程
    │ 是否有immutable?│   (If-None-Match / 
    └───────┬────────┘    If-Modified-Since)
     有     │      无
            ▼          ▼
    直接使用本地副本   普通强制缓存
    (刷新也不验证)    (刷新时可能重新验证)

📌 关键认知immutable 是强制缓存的"终极形态"。没有它时,Chrome等浏览器在用户主动点击刷新按钮时会忽略max-age并发起条件请求;有了它,即使用户刷新也不会验证。对于带内容哈希的资源,immutable 能额外消除10%~20%的304请求。

2.4 max-age vs Expires

特性 Cache-Control: max-age Expires
时间类型 相对秒数 绝对GMT时间
客户端时钟依赖 ❌ 不依赖 ✅ 依赖
HTTP版本 1.1 1.0
优先级 ⭐ 高 低(仅兜底)
推荐程度 必须设置 建议同时设置作为兼容

⚠️ 生产环境必须同时设置两者max-age 是现代浏览器的判断依据,Expires 是HTTP/1.0客户端和部分老旧CDN的兜底字段。只设其中一个都会在特定场景下失效。


三、Nginx强制缓存配置实战

3.1 按资源语义分级配置(生产模板)

复制代码
server {
    listen 80;
    root /var/www/html;

    # ========== 带内容哈希的构建产物:永久强制缓存 ==========
    # Vite/Webpack/Rspack输出格式:app.a1b2c3d4.js, style.e5f6g7h8.css
    location ~* \.[a-f0-9]{8,}\.(js|css)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # 图片/字体等二进制资源(带哈希)
    location ~* \.[a-f0-9]{8,}\.(png|jpg|jpeg|gif|svg|webp|avif|woff2?|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # ========== HTML入口文件:禁止强制缓存 ==========
    # HTML是缓存体系的锚点,必须每次都验证
    location ~* \.html?$ {
        add_header Cache-Control "no-cache, must-revalidate";
        etag on;
        last_modified on;
    }

    # ========== 不带哈希的静态资源:中等强制缓存 ==========
    location ~* \.(ico|txt|pdf|mp4|webm|mp3)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000";
        etag on;  # 强制缓存过期后走协商缓存兜底
    }

    # ========== API/动态接口:禁止一切缓存 ==========
    location /api/ {
        add_header Cache-Control "no-store";
        proxy_pass http://backend;
    }
}

3.2 六个关键配置细节深度解析

immutable 的价值被严重低估

immutable 指令告诉浏览器:"此URL对应的内容永远不会变更"。它的实际效果远超 max-age=1y

  • 普通强缓存 :用户点击刷新按钮时,Chrome会忽略max-age,发起 If-None-Match 条件请求(304往返);
  • immutable强缓存 :即使用户刷新,浏览器也完全不发请求,直接使用本地副本。

对于带内容哈希的资源,文件名本身就保证了内容的唯一性,immutable 是语义上完全正确的声明。不要省略它

add_header 的继承陷阱

这是Nginx新手最常踩的坑:add_header 不会继承到子location

复制代码
# ❌ 错误:/assets/下的Cache-Control会丢失
server {
    add_header Cache-Control "public, max-age=31536000";
    location /assets/ {
        add_header X-Custom "value";  # 上一行的Cache-Control在此处完全失效!
    }
}

# ✅ 正确:每个需要缓存头的location都必须显式设置
location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header X-Custom "value";
}

如果你希望统一管理,可以使用 map + always 参数实现集中式缓存头下发,避免遗漏。

access_log off 不是可选项

命中强制缓存的请求不到达服务器,但以下场景仍会产生日志:

  • 首次访问(缓存未建立);
  • 强制缓存过期后的条件请求(304);
  • 新部署后旧缓存失效的首次请求。

对于高频静态资源,这些日志毫无分析价值却消耗大量磁盘IO。对纯静态资源location关闭访问日志是生产环境的基本素养

④ Content Hash是强制缓存的安全前提

永久强制缓存只有在文件名包含内容哈希时才安全。如果文件名不含哈希而设置了长max-age,发版后用户将无法获取新代码,直到缓存自然过期。

确认你的构建工具开启了content hash:

javascript 复制代码
// Vite
export default defineConfig({
  build: { rollupOptions: { output: { entryFileNames: '[name].[hash].js' } } }
})

// Webpack
output: { filename: '[name].[contenthash].js' }

⚠️ 如果你的项目仍有不带哈希的JS/CSS文件,绝对不能对其设置永久强制缓存。应将其归入"中等强制缓存+协商缓存兜底"类别。

⑤ Vary头与压缩的协同

开启gzip/brotli后,同一URL可能返回不同压缩版本的响应体。如果不添加 Vary: Accept-Encoding,CDN或中间代理可能将gzip版本缓存后返回给不支持压缩的客户端,导致乱码:

复制代码
gzip on;
add_header Vary "Accept-Encoding";  # ⭐ 开启压缩时必须手动添加

Nginx不会自动添加Vary头,这是很多线上乱码问题的根源。

⑥ HTTPS下的public与private区分
复制代码
# 公共资源(CDN可缓存)
add_header Cache-Control "public, max-age=31536000, immutable";

# 私有资源(仅浏览器缓存,CDN/代理不缓存)
add_header Cache-Control "private, max-age=3600";

HTTPS响应默认被浏览器视为可缓存,但CDN和共享代理对 private 响应不会存储。涉及用户个人数据的静态资源(如头像、个人配置JSON)必须使用 private


四、与前端工程化的对齐策略

强制缓存配置不能脱离构建体系独立存在。以下是完整的对接清单:

4.1 HTML是缓存体系的锚点

整个强制缓存体系能够安全运转的前提是:HTML文件本身不被强制缓存

复制代码
location = /index.html {
    add_header Cache-Control "no-cache, must-revalidate";
    etag on;
}

工作原理链:

  1. 用户访问 → 浏览器验证index.html(304或200);
  2. 新版index.html引用 <script src="/app.a1b2c3.js">
  3. 浏览器发现本地无 app.a1b2c3.js → 下载并永久缓存;
  4. 旧版 app.x9y8z7.js 不再被引用,自然淘汰。

📌 如果HTML被强制缓存,整个链条在第1步就断裂了。这是"发版后用户看到旧页面"最常见的原因。

4.2 Service Worker脚本的特殊处理

sw.js 本身绝对不能被强制缓存,否则浏览器无法发现新版本SW,导致应用无法更新:

复制代码
location = /sw.js {
    add_header Cache-Control "no-cache, must-revalidate";
}

SW内部通过Cache API管理的资源缓存与HTTP缓存头无关,但SW文件自身的更新必须依赖协商缓存。

4.3 第三方资源的缓存策略

通过npm引入的库(如React、Vue)通常由构建工具打包进带哈希的bundle中,已自动享受永久强制缓存。但如果通过CDN script标签直接引入:

复制代码
<script src="https://cdn.example.com/react@18.2.0/umd/react.production.min.js"></script>

这类URL版本号锁定在路径中,等价于内容哈希,可以安全设置长缓存。但需注意:如果CDN地址不含版本号或哈希,不要对其设置永久强制缓存


五、调试与验证工具箱

5.1 Chrome DevTools诊断清单

打开Network面板,逐一检查:

检查项 期望值 异常信号
Size列 (disk cache) / (memory cache) 显示具体字节数 → 未命中强制缓存
Status列 200 (from disk cache) 持续200且非cache → 缓存头缺失或过期
Response Headers Cache-Control: public, max-age=31536000, immutable 缺少immutable或缺失CC头
刷新行为 刷新后Size仍为cache 刷新后变为304 → 缺少immutable
Timing面板 Cache Lookup < 1ms 过长 → 浏览器缓存索引异常

5.2 curl命令行验证

bash 复制代码
# 检查响应头完整性
curl -sI https://example.com/app.a1b2c3.js | grep -iE 'cache-control|expires|vary'

# 预期输出:
# Cache-Control: public, max-age=31536000, immutable
# Expires: Thu, 08 Jul 2027 02:42:00 GMT
# Vary: Accept-Encoding

5.3 Lighthouse审计

运行Performance审计,重点关注:

  • Serve static assets with an efficient cache policy:列出未设置长缓存的静态资源;
  • Uses immutable caching:检查带哈希资源是否缺少immutable;
  • Avoids excessive DOM size:间接反映缓存粒度是否合理。

六、常见踩坑速查表

现象 根因 解决方案
发版后用户看到旧页面 HTML被设置了强制缓存 HTML改为 no-cache,资源用content hash
用户刷新后仍产生304请求 带哈希资源缺少 immutable 添加 immutable 指令
CDN返回乱码CSS/JS 开启gzip但未加 Vary 添加 Vary: Accept-Encoding
子location缓存头不生效 add_header 不继承 每个location显式设置或用map统一管理
Safari缓存行为不一致 Safari对max-age解释有差异 同时设置 Expires 作为兜底
部分资源强制缓存不生效 响应头被后端覆盖 检查proxy_hide_header或后端CC头
新用户首次加载慢 正常现象,强制缓存需首次请求建立 配合预加载/Service Worker预热

七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
大博士.J1 小时前
视频号主体违规与认证上限?正确的解决方式
运维·python·自动化
诚信定制8392 小时前
如何启动 Redis 服务:详细步骤指南
数据库·redis·缓存
hey you~2 小时前
400电话选型技术测评:一个开发视角的线路质量评估方案
运维·网络·数据库·400电话·企业通信
筱羽_筱羽2 小时前
跟着“Ubuntu18.04/20.04编译Linux5.10.76+Xenomai-3.2.1+IgH”教程遇到的问题
linux·运维·ubuntu
DolphinScheduler社区2 小时前
Apache DolphinScheduler 6 月治理优化,补齐调度运维全链路细节
大数据·运维·云原生·apache·海豚调度
cxr8283 小时前
缓存策略探测实验方案架构设计
缓存
无忧.芙桃4 小时前
线程同步与线程互斥(上)
linux·运维·操作系统·运维开发
xywww1688 小时前
大模型 API 选型实战:GPT、Gemini、Claude 接入时该看哪些指标?
运维·服务器·人工智能·python·gpt·langchain
梦想的颜色11 小时前
【Docker原理】Docker 容器一文打尽:生命周期、环境管控、迁移备份、日志进程排查、垃圾清理
运维·docker·容器·容器生命周期·容器日志排查·容器迁移备份·容器进程管理