Service Worker 缓存无真正并发写入,所谓“冲突”实为版本混乱、覆盖或竞态,需通过命名隔离、fetch 去重更新、install 逐项缓存三策解决。

Service Worker 本身不支持“并发写入缓存”的概念——Cache API 是单线程、原子性操作,同一时刻对同一个 Cache 实例的 put() 或 addAll() 调用不会真正并发执行,而是按事件循环顺序排队处理。所谓“冲突”,实际是开发者误以为多个 fetch 请求或 install 事件会同时修改同一缓存导致数据错乱,但真实情况更常表现为版本混乱、缓存覆盖、响应竞态或 stale 数据残留。关键不在“并发写入”,而在缓存生命周期管理与策略协同设计。
下面从三个实际易出问题的场景出发,给出可落地的处理方式:
缓存命名与版本隔离避免覆盖冲突
不同版本的 Service Worker 应使用带明确版本标识的 cacheName(如 'static-v2'、'api-cache-202607'),而非固定名称(如 'cache')。否则新 SW 安装时若仍用旧名写缓存,旧缓存不会自动清除,造成资源混用。
- 每次更新 SW 脚本,同步升级 cacheName
- 在
activate阶段主动清理旧缓存
self.addEventListener('activate', event => {
const validCaches = ['static-v2', 'api-cache-202607'];
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(key => !validCaches.includes(key))
.map(key => caches.delete(key))
)
)
);
});fetch 中缓存写入需规避响应竞态
当采用“缓存并更新”(Cache & Update)策略时,常见错误是:
✅ 先返回缓存响应(快)
❌ 再 fetch() 并 cache.put() —— 若多个相同请求几乎同时触发,后发起的 put() 可能覆盖先完成的更新,导致缓存内容回退。
正确做法:对同一请求 URL 做去重+单次更新保障,例如用 Map 缓存正在更新的 Promise:
立即学习“Java免费学习笔记(深入)”;
const updating = new Map();
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// 只对 GET 请求做缓存更新
if (request.method !== 'GET') return;
event.respondWith(
caches.match(request).then(cached => {
// 有缓存就先返回
if (cached) return cached;
// 无缓存则发起网络请求
return fetch(request).then(response => {
if (!response || response.status < 200 || response.status >= 300) {
return response;
}
// 克隆响应用于缓存(响应体只能读一次)
const responseToCache = response.clone();
const cache = await caches.open('api-cache-202607');
// 避免重复 put:同一 URL 只更新一次
const key = url.href;
if (!updating.has(key)) {
updating.set(key, cache.put(request, responseToCache));
}
await updating.get(key);
updating.delete(key);
return response;
});
})
);
});install 阶段预缓存失败导致部分写入
cache.addAll(urls) 是原子操作:任一资源 404 或超时,整个缓存失败,已加载的资源不会被部分写入。这看似安全,但容易掩盖资源路径错误或 CDN 不可用问题。
更健壮的做法:逐个 fetch + put,失败项单独记录,不阻断整体缓存:
self.addEventListener('install', event => {
event.waitUntil(
(async () => {
const cache = await caches.open('static-v2');
const urls = ['/', '/app.js', '/styles.css', '/logo.png'];
for (const url of urls) {
try {
const response = await fetch(url);
if (response.ok) {
await cache.put(new Request(url), response);
}
} catch (e) {
console.warn(`预缓存失败: ${url}`, e);
}
}
})()
);
});本质上,Service Worker 的缓存不是数据库,没有事务锁或 MVCC。所谓“冲突”,根源在于未厘清缓存的作用域边界、更新时机和响应生命周期。只要坚持:
- 每版 SW 独占 cacheName
- fetch 更新加去重保护
- install 不依赖
addAll的原子幻觉
就能避开绝大多数一致性陷阱。


















