Files
TrueGrowth/docs/SMART_CDN_LOADING_LESSONS.md

159 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 智能 CDN 优先加载经验总结
## 背景
之前静态资源采用 CDN 优先策略,发版期间会出现服务器已经切到新版本,但 npm CDN 对应版本资源尚未完全可用的窗口,导致部分 JS/CSS 返回 404 或错误页,页面加载失败。后续改成源站优先后,正确性恢复,但用户感知到首屏变慢。
这次优化的目标不是“改回 CDN 绝对优先”,而是在保证发布窗口可用性的前提下,让绝大多数正常流量重新走 CDN。
## 这次采取的方案
### 1. 区分入口链路和版本化静态资源
- `index.html`、导航请求、`version.json``manifest.json``sw.js``precache-manifest.json` 继续同源优先。
- `js/css/font/image/ico/版本化 json` 改为 `Cache First -> 智能 CDN 优先 -> 源站快速兜底`
这样可以避免入口文件与静态资源来自不同版本,降低发版窗口错配风险。
### 2. 让主线程 CDN 探测真正参与 SW 决策
- 保留 `cdn-config.js` 的非阻塞测速逻辑。
- 主线程在注册/接管 Service Worker 后,把当前测速结果通过 `SW_CDN_SET_PREFERENCE` 同步给 SW。
- SW 将偏好持久化为一份很小的记录,刷新页面时即使主线程尚未完成测速,也能优先复用上次已知的 CDN 偏好。
经验:测速结果如果不进入真正的请求决策链路,只会增加复杂度,不会带来收益。
### 3. CDN 失败要“短时熔断”,避免单页重复踩坑
- CDN 对当前版本资源返回超时、404/5xx、HTML 错页、异常内容类型时,立即记为失败。
- 连续失败达到阈值后,短时间内不再优先尝试该 CDN。
- 熔断窗口过去后允许恢复探测。
经验:如果没有熔断,同一页面的几十个静态资源会重复打向已失效 CDN既慢又放大错误。
### 4. 已校验的 CDN 缓存应该允许复用
之前逻辑把“非 server 来源的静态缓存”统一视为 suspicious导致 CDN 成功拉下来的资源下次仍然可能被删掉并重新回源。
这次改为给静态缓存统一打元数据:
- `x-sw-source`
- `x-sw-revision`
- `x-sw-app-version`
只清理以下情况:
- 缺少必要元数据
- 缓存版本与当前 `APP_VERSION` 不一致
- 静态资源缓存实际返回 HTML 错页
经验CDN 缓存是否可复用,判断依据应该是“是否已被校验 + 是否与当前版本匹配”,而不是“是否来自源站”。
## 这次实现后得到的结论
### 推荐策略
- 入口链路同源优先
- 版本化静态资源智能 CDN 优先
- CDN 失败快速回源
- CDN 偏好持久化 + 健康状态熔断
- 静态缓存按版本和元数据复用
- 发布时先等主 CDN 关键资源 ready再切服务器 HTML
- 首屏必需内容不做高风险按需加载
- 构建切分优先保守,不为追求更细粒度 chunk 牺牲稳定性
### 不推荐策略
- CDN 绝对优先:发版窗口风险高
- 源站绝对优先:正常流量浪费 CDN
- 所有资源做 CDN/源站双发竞速:虽然更快,但会放大重复请求,增加服务器和 CDN 压力
- 为了压首屏包体,强行加 `manualChunks`,但没先理清循环依赖边界
## 这轮新增经验2026-04-22
### 1. 多 CDN 不是越多越稳,关键在“顺序 + 熔断”
这轮最终保留了双 CDN
- 主 CDN`jsdelivr`
- 备用 CDN`unpkg`
- 最终兜底:源站
经验:
- 多 CDN 只有在“主次明确”时才会更稳
- 如果所有 CDN 都被等权重频繁重试,失败面只会被放大
- 备用 CDN 应该是兜底,不应该在已知异常期间持续抢占请求
### 2. 连续失败要自动熔断更久,不能固定 1 分钟
之前的短时熔断只能解决“瞬时失败”,但对持续异常的备用 CDN 不够。
这轮改成按 CDN 和连续失败次数动态拉长冷却时间:
- `jsdelivr`:基础 60 秒,最长 5 分钟
- `unpkg`:基础 10 分钟,最长 1 小时
- 连续失败越多,熔断窗口越长
经验:
- 主 CDN 可以恢复得更积极
- 备用 CDN 一旦连续失败,应该更保守,避免每次首屏都被它拖慢
- 熔断策略要区分“主链路”和“备用链路”,不能一个常量打天下
### 3. 熔断必须可观测,否则线上只能猜
这轮补了两类观测信息:
- SW 日志直接打印 `failCount``reason``cooldown`
- 状态报告额外返回 `cooldownMs``cooldownUntil``remainingCooldownMs`
经验:
- 只知道“某 CDN 挂了”不够
- 还要知道“失败了几次”“为什么失败”“还要熔断多久”
- 没有剩余熔断时间,排查时很难区分是“还在熔断”还是“恢复探测又失败了”
### 4. 首屏稳定性优先于激进切分
这轮一开始尝试过继续用 `rollupOptions.manualChunks` 优化加载,但项目里存在一些动态导入和静态导入交织的边界,进一步切分后容易触发循环 chunk 风险,表现为运行时未定义或首屏卡死。
最终采取的保守结论:
- 回退激进 `manualChunks`
- 保留已有安全切分
- 首屏所需内容保证直接可加载,不依赖高风险按需拆分
经验:
- “首屏更小”不等于“首屏更稳”
- 当代码边界本身不干净时,继续切 chunk 是放大风险,不是优化
- 这类问题应该先治理依赖边界,再谈更激进的包体优化
### 5. 发布正确性的门要设在“CDN ready”之前
这轮保留了发布 gate
- npm 发版完成后
- 先等待主 CDN 关键资源可访问
- 再部署服务器侧 HTML
经验:
- 只要 HTML 比 CDN 资源先切到新版本,用户就可能卡在半加载状态
- 发版系统不能只关心“npm publish 成功”,还要关心“主 CDN 已真正可用”
- 这一步比事后依赖 SW 回退更重要,因为它直接缩小了错误窗口
## 后续可继续观察的指标
- 发版窗口内静态资源 404/HTML 错页是否明显下降
- 首屏静态资源命中 CDN 的比例是否提升
- 首次加载与二次刷新时的资源来源分布
- 某个 CDN 短时异常时,是否能稳定切到次选 CDN 或源站
## 一句话总结
CDN 优先不是问题,问题在于“没有版本边界、没有熔断、没有缓存复用规则”。把这三件事补齐,才能既快又稳。
补充一句:如果代码边界还不稳定,就不要拿 `manualChunks` 去赌首屏,先保正确性,再做更细粒度优化。