Files
TrueGrowth/docs/SMART_CDN_LOADING_LESSONS.md

6.3 KiB
Raw Blame History

智能 CDN 优先加载经验总结

背景

之前静态资源采用 CDN 优先策略,发版期间会出现服务器已经切到新版本,但 npm CDN 对应版本资源尚未完全可用的窗口,导致部分 JS/CSS 返回 404 或错误页,页面加载失败。后续改成源站优先后,正确性恢复,但用户感知到首屏变慢。

这次优化的目标不是“改回 CDN 绝对优先”,而是在保证发布窗口可用性的前提下,让绝大多数正常流量重新走 CDN。

这次采取的方案

1. 区分入口链路和版本化静态资源

  • index.html、导航请求、version.jsonmanifest.jsonsw.jsprecache-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

  • 主 CDNjsdelivr
  • 备用 CDNunpkg
  • 最终兜底:源站

经验:

  • 多 CDN 只有在“主次明确”时才会更稳
  • 如果所有 CDN 都被等权重频繁重试,失败面只会被放大
  • 备用 CDN 应该是兜底,不应该在已知异常期间持续抢占请求

2. 连续失败要自动熔断更久,不能固定 1 分钟

之前的短时熔断只能解决“瞬时失败”,但对持续异常的备用 CDN 不够。

这轮改成按 CDN 和连续失败次数动态拉长冷却时间:

  • jsdelivr:基础 60 秒,最长 5 分钟
  • unpkg:基础 10 分钟,最长 1 小时
  • 连续失败越多,熔断窗口越长

经验:

  • 主 CDN 可以恢复得更积极
  • 备用 CDN 一旦连续失败,应该更保守,避免每次首屏都被它拖慢
  • 熔断策略要区分“主链路”和“备用链路”,不能一个常量打天下

3. 熔断必须可观测,否则线上只能猜

这轮补了两类观测信息:

  • SW 日志直接打印 failCountreasoncooldown
  • 状态报告额外返回 cooldownMscooldownUntilremainingCooldownMs

经验:

  • 只知道“某 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 去赌首屏,先保正确性,再做更细粒度优化。