Skip to content

NGINX 缓存

情况背景

你是否也经历过这样的场景:刚刚加班加点修复了一个线上紧急 Bug,满怀信心地通知用户刷新页面,得到的反馈却是“问题依旧”。你无奈地回复:“请您清一下浏览器缓存或者开无痕模式试试”,内心却在咆哮:都 2025 年了,为什么我的现代化网页应用还需要用户手动清缓存?

标准的缓存失效策略

正常情况下项目中通常采用的“标准”缓存失效策略是采用以下方法:

  • 文件名 Hash 化

  • HTML 入口文件防缓存

文件名 Hash 化

使用 Vite、Webpack 等构建工具,在打包时为 JS、CSS 等静态资源生成独一无二的 Hash 值文件名。

Vite和Webpack在默认配置下,对构建输出的文件处理方式有所不同,但都可以实现生成带有hash值后缀的文件,不过并不是每次打包都会重新生成全新的hash值,具体情况如下:

Vite

  • 默认情况

    在Vite中,默认会对构建输出的静态资源(js、css等)进行哈希处理,生成一个基于文件内容的哈希值,并添加到文件名后缀中。但是,只有当文件内容发生改变时,才会重新生成新的hash值。

    比如你开发一个Vue3项目,有一个组件的逻辑代码没有修改,Vite在打包时,该组件对应的js文件的hash值就不会改变。这是因为Vite基于ESBuild进行打包,它对文件的变化检测很高效,只有文件内容有实质性变动,才会重新计算并生成新的hash。

  • 缓存 busting

    这种机制可以很好地利用浏览器缓存。当你部署新版本应用时,未修改的文件由于hash值不变,浏览器会直接从缓存中读取,提升加载性能;而修改过的文件hash值改变,浏览器会重新请求新文件,保证用户获取到最新内容。

  • 配置自定义

    你还可以通过插件或者配置项来自定义哈希行为。例如使用rollup-plugin-hash等插件,进一步调整哈希的生成规则,或者控制哪些文件需要生成哈希、哈希值的长度等 。

Webpack

  • 默认情况

    Webpack本身没有默认直接为所有输出文件生成基于内容的hash。但在配置output.filenameoutput.chunkFilename时,通过使用[contenthash]占位符,可以实现根据文件内容生成hash值。

    当文件内容没有变化时,hash值不会重新生成。比如在一个React项目中,你只是修改了某个组件的注释,并没有改变实际的代码逻辑,那么该组件打包后的js文件的contenthash值不会改变 。

  • chunkhash和hash

    Webpack中还有[hash]占位符,它是根据整个构建过程的上下文计算出来的哈希值,只要构建过程中有任何变化(包括不同文件的增删改),所有使用[hash]的文件都会生成新的hash值。而[contenthash]更精准,仅根据单个文件内容生成,推荐在生产环境中使用[contenthash]来优化缓存。

  • 插件和loader的影响

    此外,Webpack丰富的插件生态系统也会影响哈希生成。比如terser-webpack-plugin在压缩代码时,如果配置不当,可能会导致即使文件内容逻辑未变,但因为压缩后的格式微调等原因,生成新的contenthash。同时,一些loader在处理文件时,也可能间接影响文件内容从而影响哈希值的生成 。

HTML 入口文件防缓存

在 index.html 的 <head> 部分,加上能够禁止缓存的 <meta> 标签

html
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">

这两个方法实现思路是:

  1. index.html不被缓存,每次访问都是最新的。

  2. 最新的index.html引用了带新 Hash 的 JS/CSS 文件。

  3. 浏览器发现文件名变了,自然会去加载新资源。

  4. 这样用户看到了最新的页面。

但是在实际项目情况当中,每次重新打包更新前端后,用户刷新还是访问的旧版本的前端内容。

失效原因

主要是因为浏览器缓存优先级问题。

HTTP 响应头(Response Headers)的优先级 > HTML Meta 标签

所以 meta 标签更像是一种“建议”

而由服务器(如 Nginx)在 HTTP 响应中返回的 Cache-Control、Expires 等头部信息,才是浏览器必须严格遵守的“最高指令”。

如果 HTTP 响应头没有明确指示不缓存,或者指示了可以缓存,那么浏览器就会愉快地忽略 meta 标签的建议,将 index.html 缓存起来。

一般的NGINX配置文件nginx.conf,都没有去管理响应头,都是采用默认的方式。

nginx
location / {
    try_files $uri $uri/ /index.html;
    # 🔴 致命的遗漏:这里没有任何关于 index.html 的缓存控制指令!
}

所以用户浏览器访问会变成:

  • 当浏览器请求 https://yoursite.com/ 时,命中了 location / 规则。

  • Nginx 返回了 index.html 文件,但没有附加任何 Cache-Control 或 Expires 响应头。

  • 浏览器或上游 CDN 看到这个“沉默”的响应,便启用自己的默认缓存策略,将 index.html 缓存了一段时间。

  • 当你部署新版本后,JS 文件名(例如 main.new-hash.js)虽然变了,但用户再次访问时,浏览器直接从缓存中取出了旧的 index.html。

  • 旧的 HTML 文件依然引用着旧的 JS 文件(main.old-hash.js)。

  • 最终,用户看到的还是旧版本,那个 Bug 依然存在。

<meta>标签根本没有任何存在感。

解决方案

那就只要给 NGINX 为不同类型的文件下达明确的缓存策略就能解决。

可用于生产环境的NGINX配置方案:

NGINX
server {
    listen 80;
    server_name your.domain.com; # 替换为你的域名
    root /usr/share/nginx/html; # 替换为你的项目根目录

    # 规则1:HTML 文件 - 永不缓存
    # 这是最关键的一步,确保浏览器总是获取最新的入口文件。
    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires "0";
    }

    # 规则2:带 Hash 的静态资源 - 永久缓存
    # 文件名中的 Hash 确保了内容变化时文件名也会变化,所以可以放心地让浏览器永久缓存。
    # `immutable` 告诉浏览器这个文件内容永远不会变,连校验请求都无需发送。
    location ~* \.[a-f0-9]{8}\.(css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 规则3:其他静态资源(如图片、字体) - 长期缓存
    # 这些文件通常不带 Hash,但也不常变动,可以设置一个较长的缓存时间。
    location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf)$ {
        expires 30d;
        add_header Cache-Control "public";
    }

    # 规则4:单页应用(SPA)路由处理
    # 这是保证 React/Vue 等路由正常工作的关键。
    # 重要的是,它会将所有未匹配到具体文件的请求都交由 index.html 处理。
    # 由于我们已为 /index.html 单独设置了不缓存规则,所以这里是安全的。
    location / {
        try_files $uri $uri/ /index.html;
    }
}

配置解读

  • location = /index.html:使用 = 精确匹配 /index.html,并强制其不被任何一方缓存。这是整个策略的核心。

  • location ~* \.[a-f0-9]{8}\.(css|js)$:通过正则表达式匹配所有带 8 位 Hash 的 JS 和 CSS 文件,并设置长达一年的缓存(1y)和 immutable 属性,实现最佳性能。

  • location /:作为最后的 fallback,处理 SPA 的前端路由,将所有页面导航都指向不缓存的 index.html。

将这份配置应用到你的 nginx.development.conf, nginx.testing.conf, nginx.production.conf, 和 nginx.preview.conf 文件中(根据不同环境微调 expires 时间即可),你将彻底告别缓存带来的烦恼。

文章参考:

什么?2025年了发版后还要手动清浏览器缓存?