0%

Service Worker 缓存策略

为什么需要缓存策略

即便使用了 Service Worker,如何准确缓存资源、更新逻辑以及清除过期内容,都是开发者必须应对的挑战。未优化的缓存策略会导致旧资源长期存在、用户获取不到最新内容,甚至出现版本冲突。

本篇将从基础到进阶详解多种常见缓存策略,讨论它们的适用场景、优缺点,并逐步构建一套可扩展的缓存体系。

核心 API 回顾

在开始之前,复习一下 Service Worker 提供的缓存相关 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const cacheName = 'my-app-v1';

self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll([
'/index.html',
'/styles/main.css',
'/scripts/app.js'
]))
);
});

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});

caches.matchcaches.opencache.putcache.delete 是组成缓存策略的基本工具。

策略一:Cache First

最简单的策略:优先从缓存读取,缓存命中则直接返回,否则请求网络并缓存结果。

适用场景:静态资源、图片、第三方库等。

优点:离线可用、速度快。

缺点:更新复杂,需要手动管理缓存版本。

完整实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cacheName = 'static-v1';

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then(networkResponse => {
if (networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(cacheName).then(cache => {
cache.put(event.request, responseClone);
});
}
return networkResponse;
});
})
);
});

策略二:Network First

优先尝试从网络获取,只有在失败时才回退到缓存。

适用于:API 数据、新闻、动态内容。

优点:保证数据最新。

缺点:网络延迟大时体验差;离线时可能无响应(除非缓存了上一次数据)。

实现示例:

1
2
3
4
5
6
7
8
9
10
11
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
const responseClone = networkResponse.clone();
caches.open(cacheName).then(cache => cache.put(event.request, responseClone));
return networkResponse;
})
.catch(() => caches.match(event.request))
);
});

策略三:Stale-while-revalidate

既返回缓存内容提升速度,同时异步请求最新资源并更新缓存。

适用于需要平衡快速响应与数据更新的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener('fetch', event => {
event.respondWith(
caches.open(cacheName).then(cache => {
return cache.match(event.request).then(cachedResponse => {
const networkFetch = fetch(event.request).then(networkResponse => {
if (networkResponse.status === 200) {
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
});
return cachedResponse || networkFetch;
});
})
);
});

策略四:Cache and Network Race

同时发起缓存和网络请求,只要先返回者即可,另一个仍然需要写入缓存。

适合对时效要求高,并且可以容忍某些冗余请求的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
self.addEventListener('fetch', event => {
event.respondWith(
new Promise((resolve, reject) => {
let rejected = false;
const abortController = new AbortController();

const failOnce = () => {
if (rejected) {
reject('both failed');
} else {
rejected = true;
}
};

caches.match(event.request).then(response => {
if (response) {
resolve(response);
} else {
failOnce();
}
}).catch(failOnce);

fetch(event.request, { signal: abortController.signal })
.then(networkResponse => {
resolve(networkResponse);
caches.open(cacheName).then(cache => cache.put(event.request, networkResponse.clone()));
abortController.abort();
})
.catch(failOnce);
})
);
});

缓存版本管理与清理

当应用升级时,旧缓存可能过时。常见做法是在 installactivate 事件中删除旧缓存:

1
2
3
4
5
6
7
8
9
10
self.addEventListener('activate', event => {
const currentCaches = ['static-v2', 'dynamic-v1'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(name => !currentCaches.includes(name)).map(name => caches.delete(name))
);
})
);
});

此外,可以为缓存资源设置 maxEntriesmaxAgeSeconds,通过定期清理确保不占用过多空间。

离线优先策略组合

对于绝大多数前端应用,我们会采用混合策略:

  1. 基本静态资源 使用 Cache First
  2. API 数据 使用 Network First 或 Stale-while-revalidate
  3. 大型媒体 可采用 Cache First 并加上容量控制

通过配置不同的路由规则(如通过 workbox 插件)可简化工
具化配置。下面示例展示 Workbox 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// workbox-config.js
module.exports = {
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
{
urlPattern: /^https?:\/\/(api|another)/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
},
},
],
};

监控与分析

缺陷的缓存策略常常表现为:

  • 用户始终看到旧版本页面
  • 缓存不断膨胀导致存储配额耗尽

可通过在 fetch 事件中添加日志、结合 clients.matchAll 检查缓存状态,或者向后端发送异常报告来监控。

进阶技巧

  • 缓存优先级:为不同资源设定权重,优先清理低优先级内容。
  • 条件更新:根据 HTTP 头部(如 ETag)判断资源是否改变,减少网络开销。
  • 外部 CDN 缓存:结合 Service Worker 与 CDN 缓存可以双重加速。

总结

Service Worker 缓存策略不是一刀切,需要根据应用特点灵活选择。本文介绍的几种策略可作为参考,实际项目中常常结合使用,并通过版本控制、监控与清理机制保持缓存健康。掌握这些技巧,可以让你的前端在断网、慢网环境下表现更优秀,同时避免资源过期带来的问题。

继续探索更多缓存优化方案,将显著提升用户体验和系统稳定性。