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