问题描述
线上页面首次加载时出现明显的样式闪烁(Flash of Unstyled Content,FOUC),刷新后恢复正常。同时发现 favicon 的随机性也不生效——明明配置了随机宝可梦 icon,线上却一直显示同一个。
这是典型的”刷新正常,首次加载异常”模式。根据 FOUC 的诊断经验:首屏巨大化加上刷新后正常,几乎可以锁定是 CSS 加载时机问题。
诊断过程
首先对比 / 和 /zh/ 两个 URL 的 HTML 输出,发现两者的 DOM 结构完全相同,但表现不同。这排除了解析层面的问题。
接下来检查 CSS 内联情况。通过对比实验发现:内联 CSS 变量(Layer 1)可以正常生效,但 Tailwind 生成的 utility class 规则(Layer 2)没有内联,导致样式丢失。
进一步分析发现,Service Worker 缓存了一个旧版本的 HTML,其中缺少了内联的 CSS 规则。这解释了为什么”部署后还是 FOUC”——不是新代码有问题,而是浏览器拿到的是缓存中的旧版本。
踩坑记录
第一个坑是 CSS 内联修复后的部署时机。修复方案是将 Tailwind 的 utility class 也内联到 HTML 中,避免外部 CSS 文件加载前的 FOUC。部署完成后满心欢喜刷新,结果还是 FOUC。原因是 Service Worker 缓存了旧版 HTML。
第二个坑是 jsDelivr 的 @main 标签缓存问题。favicon 使用了 jsDelivr 的 @main 标签来加载 pokesprite 资源包,这个标签在构建时被解析为具体的 commit hash。但 CDN 缓存层会忽略这个 hash,导致即使更新了 @main 指向的版本,CDN 依然返回旧资源。结果是”随机宝可梦 favicon”变成了”固定宝可梦 favicon”。
解决方案
对于 Service Worker 缓存问题,关键是打破缓存链:
- 在 HTML 中添加时间戳或版本号作为查询参数
- 更新 Service Worker 的缓存策略,对 HTML 使用
no-cache或network-first - 清除旧缓存:
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' })
对于 jsDelivr 缓存问题,有两种解法:
- 使用完整的 commit hash 替代
@main标签(但每次更新需要手动修改) - 在资源 URL 后添加时间戳查询参数,例如
?t=1699999999
// Service Worker 缓存清除
self.addEventListener('message', (event) => {
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
教训
同一个问题为什么要修两次?因为第一次只修复了表面症状(CSS 内联),没有诊断到根本原因(Service Worker 缓存)。这给了我一个重要教训:技术层验证通过不等于症状消失。
Build 成功、TypeScript 类型检查通过、curl 返回 200——这些都只是技术层的证据。真正的症状消失需要实际用户场景的验证。对于 FOUC,这意味着必须在禁用缓存或使用隐私模式下首次加载。
更深层的教训是:当一个问题反复出现时,首先要问的不是”怎么修”,而是”为什么第一次没修好”。是验证是假的?还是诊断不完整?只有回答了这个问题,才能避免下一次的重蹈覆辙。