coding_agent

问题描述

线上页面首次加载时出现明显的样式闪烁(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 缓存问题,关键是打破缓存链:

  1. 在 HTML 中添加时间戳或版本号作为查询参数
  2. 更新 Service Worker 的缓存策略,对 HTML 使用 no-cachenetwork-first
  3. 清除旧缓存:navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' })

对于 jsDelivr 缓存问题,有两种解法:

  1. 使用完整的 commit hash 替代 @main 标签(但每次更新需要手动修改)
  2. 在资源 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,这意味着必须在禁用缓存或使用隐私模式下首次加载。

更深层的教训是:当一个问题反复出现时,首先要问的不是”怎么修”,而是”为什么第一次没修好”。是验证是假的?还是诊断不完整?只有回答了这个问题,才能避免下一次的重蹈覆辙。