你以为模块加载失败刷新一下就好了?Chrome 156 今天把失败永久缓存从根上修掉了
你的网站用户在地铁里打开页面,某个 CDN 的 JS 模块恰好超时了。浏览器报了一个 404,整个页面的 import() 就永久死掉了——即使网络恢复、CDN 恢复正常,用户刷新一百次还是得到同样的错误。这个从浏览器第一天支持 ES Module 就存在的「永久缓存失败」行为,今天被 Chrome 156 彻底修掉了。
问题:模块失败为什么会永久缓存
ES Module 有自己的模块记录(Module Map)机制。每个模块 URL 在模块 map 里只会创建一次记录,无论成功还是失败。一旦某个 URL 的 import() 触发了网络错误,这个 URL 对应的模块记录就被永久标记为失败,后续所有对同一 URL 的 import() 调用会立即返回同样的错误,不会再发任何网络请求。
// 用户在地铁里,网络恰好抖动
const r = await import('./lazy-module.js'); // 抛出 Error: Failed to fetch
console.log(r); // 永远是 undefined,哪怕你等 30 分钟再试
这个问题从 ES Module 进入浏览器第一天就存在,三大引擎无一例外。
根子:模块 map 缓存的是「结果」而不是「状态」
ES Module 的模块 map 是一个 URL → Promise 的映射表。模块记录创建后,这个 Promise 的状态(resolved / rejected)就被固定了。规范在最初设计时,没有区分「模块不存在(404)」和「此刻网络不可达」,两种情况的处理是一样的——永久记住失败。
修复:WHATWG HTML PR #10327 的改动
WHATWG HTML PR #10327 修改了浏览器模块加载器对网络错误的处理逻辑:HTTP 错误响应(4xx/5xx)不再被缓存到模块 map 中。
| 浏览器 | 版本 | 状态 |
|---|---|---|
| Chrome | 156+ | 2026-09-30 稳定版 |
| Firefox | 155+ | 已发布 |
| Safari | 18.2+ | 已发布 |
三大引擎全部实现。
三个典型场景直接受益
场景一:CDN 临时故障,模块自动恢复
某个第三方 SDK 的 CDN 在高峰期响应变慢,偶尔返回 502。以前一旦某次 import() 触发 502,用户就必须清除浏览器缓存才能恢复。现在,网络恢复后模块会自动重新加载。
场景二:Service Worker 更新期间,模块加载失败
PWA 的 Worker 里 import() 的动态模块一旦失败,同样会被永久缓存。Chrome 156 之后,Service Worker 可以在下次网络条件好的时候重新加载。
场景三:弱网环境下,用户移动时网络切换
用户从 WiFi 切换到移动数据,某些资源 URL 可能短暂不可达。以前这意味着「永远不可达」,现在换个网络环境就能恢复。
Before / After 对比
// ===== Before (Chrome 155 及以前) =====
// 第一次失败 → 永久缓存 → 永远失败
const r1 = await import('./module.js'); // Error
const r2 = await import('./module.js'); // 同一个 Error,不发网络请求
// ===== After (Chrome 156 / Firefox 155+ / Safari 18.2+) =====
// 第一次失败 → 不缓存 → 网络恢复后可重试成功
const r1 = await import('./module.js'); // Error
const r2 = await import('./module.js'); // Success
这是底层缓存行为变化,现有代码不需要任何改动,但自动获得「重试即恢复」的能力。
下一步
- 如果你维护 PWA 或 Service Worker:检查你的模块加载逻辑里有没有依赖「失败即永久失败」假设的代码。
- 如果你在弱网环境下测试 Web 应用:以前需要清除缓存才能重现模块加载失败,现在这个失败会自动恢复,测试场景需要相应调整。
评论区
登录后可评论。