# 智能 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` 去赌首屏,先保正确性,再做更细粒度优化。