SvelteKit SSG 发布后,为什么老访客非要强刷才能看到新内容?

一次从页面显示「产品即将上线,敬请期待」查到 Nginx 缓存头的真实排障。涉及 __data.json、浏览器启发式缓存、no-cache 的真正含义,以及一份不误伤静态资源的 Nginx 配置。

技术栈:SvelteKit 2 + Svelte 5 + @sveltejs/adapter-static,build:ssg 生成纯静态站,Nginx 直供 build-static/,后台点「一键发布」重新构建并覆盖产物。文中域名与路径已做脱敏。

1. 现象

发布完成后访问 https://example.com/rd-center#products,产品区显示空态:

text 复制代码
产品即将上线,敬请期待

按一下 F5,6 个产品全出来了。开无痕窗口,一切正常。

第一反应通常是「构建没把数据打进去」。但这次不是。

2. 先确认静态产物里到底有没有数据

SvelteKit 预渲染会同时产出两样东西:

文件 内容
rd-center.html 完整 HTML,服务端 load 的数据以 devalue 编码内联在页面里
rd-center/__data.json 同一份 load 数据,供客户端导航时取用

__data.json 不是普通 JSON,而是 devalue 的扁平数组:

json 复制代码
{"type":"data","nodes":[{"type":"data","data":[...],"uses":{...}}]}

用项目里已有的 devalue 解出来即可:

js 复制代码
import { unflatten } from 'devalue';
import fs from 'fs';

const d = JSON.parse(fs.readFileSync('rd-center/__data.json', 'utf8'));
const page = unflatten(d.nodes[1].data);
console.log(page.templateData.products.length); // 6

再看响应头:

bash 复制代码
curl -I https://example.com/rd-center
# HTTP/1.1 200 OK
# Last-Modified: Tue, 10 Nov 2026 13:46:25 GMT
# ETag: "6af32031-ce67"
# (没有 Cache-Control)

结论:产物是新的、数据是全的、服务器返回 200。无痕窗口正常也印证了这点。问题出在浏览器手里那份旧副本。

3. 一个页面,两份数据

理解这个现象,关键是 SvelteKit 有两种拿到数据的方式:

进入方式 数据从哪来
直接输网址 / F5 / 无痕窗口 rd-center.html,数据内联在 HTML 里
站内点链接(客户端导航) 另发一个请求取 /rd-center/__data.json?x-sveltekit-invalidated=01

而 Nginx 对静态文件默认只发 Last-Modified + ETag,不发 Cache-Control 。没有明确过期时间时,RFC 7234 允许浏览器自己估算一个「新鲜期」(常见做法是按 Date - Last-Modified 取一个比例)。这个估算结果可能是几小时甚至更久。

于是发布之后:

  • 浏览器认为旧 HTML 还新鲜 → 连条件请求都不发 → 一直渲染旧页面里内联的空产品数组
  • F5 通常会强制重新验证 → 拿到 304 或新 HTML → 产品出现
  • 站内导航同理,用的是可能被缓存的旧 __data.json

「刷新一次就好」不是数据本来就缺,而是刷新恰好触发了重新验证。 只要没人给这些响应定规矩,下一次发布还会重演。

4. 第一层:管住数据小文件

先给 __data.json 定规矩:

nginx 复制代码
location ~ __data\.json$ {
    add_header Cache-Control "no-cache" always;
}

这里踩过一个坑,值得单独说:我最初顺手加了 try_files $uri =404;,但这台服务器的 404 兜底是 error_page 404 /index.html;(SPA 式兜底)。万一文件真缺了,会先 404、再被兜底成首页 HTML------客户端盼着 JSON,等来一个网页,报错信息完全跑偏,比不加还难查。

结论:这个 location 里不要写 try_files,让 Nginx 用默认的静态文件处理即可,它本来就会正确返回文件或 404。

5. 第二层:管住网页 HTML(难点)

想让发布后连强刷都省掉,得给 HTML 也加同样规矩。但有两个障碍:

  1. 页面 URL 不带 .html 后缀 :/rd-center、/about、/rd-center/product/1。而 location ~ \.html$ 匹配的是原始请求 URI ,永远匹配不到 /rd-center。
  2. 不能误伤静态资源 :如果直接在 location / 里写死 no-cache,走这个 location 的 JS、CSS、图片也一起变成每次回源,带 hash 的 immutable 资源就白缓存了。

解法是按网址形态分流:带静态资源后缀的、位于 /_app/ 和 /media/ 的,返回空串;其余(页面路径 + __data.json)返回 no-cache。

完整配置

第一段放在 server { ... } 外面 ,conf.d/*.conf 里直接写在 server { 之前即可:

nginx 复制代码
# 按网址形态决定缓存规矩:页面每次确认,静态文件照旧缓存
map $uri $cache_rule {
    default                                                     "no-cache";
    ~*\.(js|css|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|map)$  "";
    ~*^/_app/                                                   "";
    ~*^/media/                                                  "";
}

server {
    # ... ssl / server_name 等原有配置 ...
    root /srv/your-site/build-static;
    index index.html;

    # 新增这一行,由各 location 继承
    add_header Cache-Control $cache_rule always;

    location /media/ { alias /srv/your-site/media/; }

    # 与上面的 map 效果重叠,可留可删
    location ~ __data\.json$ {
        add_header Cache-Control "no-cache" always;
    }

    # 以下保持你原有的写法,不要照抄
    location / { try_files $uri $uri.html $uri/index.html /index.html; }
    error_page 404 /index.html;
    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}

map 分流的结果:

请求 $cache_rule 效果
/rd-center、/about、/rd-center/product/1 no-cache 每次用前确认
/rd-center/__data.json no-cache 每次用前确认
/_app/immutable/chunks/xxx.js、/logo.png "" 不加这个头,沿用原缓存
/media/xxx.jpg "" 不加这个头

Nginx 的 add_header 在求值为空字符串时不添加该头 ,所以返回 "" 等于「不插手」,不是加了个空头。

6. 四个容易翻车的细节

no-cache 不是「不许缓存」。 它是「可以存,但每次用之前必须回源确认」。没变回 304(几乎不传内容),变了才回 200 + 新内容。要完全不存得用 no-store,对这个场景没必要。

map 只能写在 http 上下文。 塞进 server {} 会报 map directive is not allowed here。因为 conf.d/*.conf 是被 nginx.conf 的 http {} include 进来的,写在 server 块前面就合法。

add_header 的继承规则。 某一层只要自己写了 add_header,就不再继承上一层。如果 location / { ... } 里本来就有别的 add_header(比如安全头),server 级那行对它无效 ------ 把 add_header Cache-Control $cache_rule always; 也照抄一份进那个 location。

/media/ 别顺手设长缓存。 后台上传图片可能是同名覆盖,一旦给了 immutable 或很长的 max-age,管理员换了图,访客几周都看不到新的。让它保持原样最安全。

另外,正则 location 的优先级高于普通前缀 location,但低于精确匹配 location = /50x.html。新增的 __data.json 正则块不会抢走 /50x.html;如果配置里还有其他正则 location,注意它们的先后顺序(正则按出现顺序匹配,先写先命中)。

7. 验证

bash 复制代码
nginx -t && systemctl reload nginx

三条请求各看各的:

bash 复制代码
curl -I https://example.com/rd-center
# Cache-Control: no-cache          ← 页面,对了

curl -I https://example.com/logo.png
# Expires: Thu, 10 Dec 2026 14:23:05 GMT
# Cache-Control: max-age=2592000
# Cache-Control: public, no-transform
# (没有 no-cache)                ← 图片没被误伤,对了

curl -I https://example.com/rd-center/__data.json
# Cache-Control: no-cache          ← 数据文件,对了

图片那两行 Cache-Control 是服务器原有配置(expires 30d 生成 max-age,另一处 add_header 加了 public, no-transform),浏览器会合并处理,不是这次引入的。判断有没有误伤只看一点:里面有没有 no-cache。

8. 效果,以及最后一次强刷

配置生效后:

  • 老访客打开页面 → 带 If-None-Match 问一句,没变回 304,变了拿新页面
  • 老访客站内点链接 → 数据小文件同样走确认
  • 图片、CSS、JS → 照旧长期缓存,不多花流量

但要注意:no-cache 只对之后收到的响应 生效。浏览器里已经存着的那份旧 HTML(当初没有任何 Cache-Control)可能仍被判定新鲜,连问都不问。所以配置上线后,自己和相关同事再强刷最后一次就彻底干净了,从那以后不用再刷。

小结

  1. 「刷新就好」不等于数据缺失,先解码 __data.json、看响应头,别急着怀疑构建。
  2. Nginx 不给 Cache-Control,浏览器就会用启发式缓存自己估新鲜期,发布后的旧页面能赖很久。
  3. no-cache = 每次问一句,靠 304 兜住流量,是这类场景的正解。
  4. SvelteKit 静态站的页面 URL 没有 .html 后缀,用 map $uri 按网址形态分流,才能既管住页面又不误伤静态资源。
  5. 配置只管将来,已经缓存的旧页面仍需强刷一次收尾。
相关推荐
鸿蒙开发19 分钟前
小程序存档写到一半怎么办?双槽保存与损坏恢复实战
前端·javascript·react.js
卸任1 小时前
AI 编程工具越用越多,模型配置越来越乱:用 Magpie 把它们管起来
前端·ai编程
前端一课1 小时前
凝神诀
前端
前端双越老师2 小时前
程序员 1v1 访谈系列第十三期:大龄已育空窗一年如何破局
前端·ai编程·全栈
恋猫de小郭3 小时前
Flutter 大小核优化又被回滚,然后又开新提案?
android·前端·flutter
MyBili3 小时前
Chrome扩展开发入门:从零打造实用浏览器插件
前端·chrome·谷歌浏览器·效率工具·浏览器插件·前端入门·扩展开发
FungLeo3 小时前
成为全栈·Vue 管理后台篇·Vue3 + Vite + Naive UI:后台工程基座怎么搭
前端·vue.js·ui·typescript·vite·前端工程化·biome
风骏时光牛马3 小时前
AI_Coding:智能代码创作引擎
前端