
记录一次生产环境的缓存踩坑:chunk 404、CSS 无样式闪烁、图片每次重载——三个看似不同的问题,根源都指向同一套缓存配置的漏洞。
这个博客网站部署上线之后,收到了一些奇怪的反馈:页面有时候没有样式,图片每次刷新都重新加载,偶尔控制台还会报一个看起来很严重的 ChunkLoadError。
三个问题,排查下来发现根源高度重合,值得一次性记录清楚。
问题现象
打开浏览器 DevTools,能看到三类错误交替出现:
GET /_next/static/chunks/756bf93382790689.js 404GET /_next/static/chunks/9d69d64a9fe60bc3.css 404Uncaught ChunkLoadError: Failed to load chunk
同时视觉上有两个症状:
- FOUC(Flash of Unstyled Content):页面先以无样式状态渲染,CSS 加载后才"跳"到正常样式
- 图片每次重载:每次访问主页,所有图片都重新从网络加载,没有走缓存
根因分析
问题一:微缓存 updating 导致旧 HTML 泄漏
nginx 配置了 30 秒的页面微缓存,同时开启了 proxy_cache_use_stale updating:
proxy_cache_valid 200 301 302 30s;proxy_cache_use_stale error timeout ... updating;
updating 的语义是:缓存过期后,第一个请求立即返回旧缓存,同时后台异步拉取新内容。
问题在于:每次部署,旧缓存的 HTML 里引用的是旧构建的 chunk 哈希(如 756bf93382790689.js)。新部署后这些文件已不存在,但浏览器拿到旧 HTML 后会去请求它们,得到 404。
缓存 30s 过期 → 新请求进来→ nginx 立刻返回【旧 HTML】(含旧 chunk 哈希) ← 罪魁祸首→ 后台拉取新 HTML→ 浏览器用旧 HTML 请求旧 chunk → 404
问题二:CSS chunk 404 是静默失败 JS 的 ChunkLoadError 会被 React 的 Error Boundary 捕获,可以通过 error.tsx 处理。但 CSS 资源 404 是静默失败——浏览器不会抛出 JS 异常,只是默默忽略,样式丢失,这就是 FOUC 的来源。
两种 chunk 错误的处理路径完全不同,容易只修了 JS 那边而忽略 CSS。
问题三:/public/img/ 文件被 Next.js 发送 max-age=0 Next.js 对 /_next/static/ 下的文件(有内容哈希)会发送长缓存头,但对 /public/ 目录下的静态文件(无哈希,文件名固定),默认发送:
Cache-Control: public, max-age=0
这是合理的默认行为——文件名不变但内容可能更新,所以不敢设长缓存。
但我的 nginx 配置里 /img/ 没有专属 location,所有图片请求都落进 location / 的通用块,nginx 把这个 max-age=0 原样透传给浏览器。结果:浏览器每次访问都需要重新验证,图片看起来像是"重新加载"。
解决方案
- 移除 updating,避免旧 HTML 泄漏
# 之前proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504 updating;# 之后:移除 updatingproxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504;
代价是缓存过期后第一个请求需要等待上游响应,但消灭了旧 HTML 泄漏的窗口。
2. 为 /img/ 添加专属 location,覆盖 Next.js 的 max-age=0
location ~* ^/(img|favicon\.ico)(/|$) {proxy_pass http://gump_next;proxy_http_version 1.1;proxy_set_header Host $host;proxy_cache gump_cache;proxy_cache_key "$scheme$host$request_uri";proxy_cache_valid 200 7d;proxy_cache_lock on;proxy_ignore_headers Set-Cookie Cache-Control; # 关键:忽略上游的 max-age=0expires 7d;add_header Cache-Control "public, max-age=604800" always;add_header X-Cache-Status $upstream_cache_status always;}
proxy_ignore_headers Cache-Control 让 nginx 忽略 Next.js 返回的缓存头,由 nginx 自己决定下发给浏览器的策略。
3. 为 /uploads/ 补充 nginx 层缓存
上传图片通过 API 路由 /api/uploads/ 提供服务,API 代码里已经正确设置了 immutable 头,但 nginx 没有专属 location,导致每次都穿透到 Next.js 进程:
location ^~ /uploads/ {proxy_pass http://gump_next;proxy_http_version 1.1;proxy_set_header Host $host;proxy_cache gump_cache;proxy_cache_key "$scheme$host$request_uri";proxy_cache_valid 200 7d;proxy_cache_lock on;proxy_ignore_headers Set-Cookie;add_header X-Cache-Status $upstream_cache_status always;}
4. 背景图从 CSS background-image 改为 <Image priority>
Travel 模块的背景图通过内联样式设置:
// 之前:CSS background-image,不会被预加载style={{ backgroundImage: `url('${backgroundImageUrl}')` }}
CSS 背景图不在浏览器的预加载扫描(Preload Scanner)范围内,HTML 解析完才开始下载,视觉上就是"晚出现"。
改成 Next.js <Image priority> 后,框架会在 <head> 注入 <link rel="preload">,图片和 HTML 并行下载:
// 之后:和 HTML 并行预加载<Image src={backgroundImageUrl} fill priority sizes="50vw" />
排查工具
几个在这次排查中有用的方法:
X-Cache-Status响应头:配置里加了add_header X-Cache-Status $upstream_cache_status always,在 DevTools → Network 里可以直接看到每个请求是HIT、MISS还是BYPASS- 强制刷新(Ctrl+Shift+R):绕过浏览器缓存,快速验证 nginx 层的行为
- Network 面板的
Size列:(memory cache)/(disk cache)表示走了浏览器缓存,数字大小表示从网络下载
小结
| 问题 | 根因 | 修复 |
|---|---|---|
| ChunkLoadError / CSS 无样式 | nginx updating 返回含旧 chunk 哈希的 HTML | 移除 updating |
| 图片每次重载 | /img/ 缺专属 location,Next.js 的 max-age=0 被透传 | 加 location + proxy_ignore_headers Cache-Control |
| 背景图晚加载 | CSS background-image 不进预加载扫描 | 改用 <Image priority> |
三个问题的排查路径不同,但修法有一个共同思路:nginx 的 location 匹配要精确,不能让静态资源走通用块。