是谁还在学 Service Worker
Category(分类): HTML Status: 未知
本文尽量保留原文的叙述顺序和主题,对 Service Worker、Web Worker、Chrome Extension Service Worker、缓存策略和 Workbox 进行了合并整理。原文中的代码复制残留、过时或不准确的生命周期描述已经删除或修正。
前言
最近给平台做性能优化,恶补了一波 Service Worker,发现它的应用场景远不止性能优化和缓存。如果想做一些离线、后台事件和通知相关的功能,Service Worker 确实可以为浏览器端提供更多能力。
不过 Service Worker 的上手成本并不低。如果有人问你:
- Service Worker 和 Web Worker 有什么关系?
- Service Worker 和浏览器插件里的
background.js是一回事吗? - Service Worker 是不是一个始终运行的后台线程?
- Service Worker 能不能拦截所有请求?
这些问题很容易混淆。本文将尽量用直观的方式介绍 Service Worker 能做什么,以及它与相关概念之间的区别。
Service Worker 是什么?
Service Worker 是一种事件驱动的 Worker。它可以在满足安全上下文、作用域和页面控制条件时,作为 Web 应用、浏览器与网络之间的代理层,处理缓存、离线、Push 和部分后台事件。
Service Worker 可以理解成一个由浏览器管理的“中介”:
- 用户暂时没有网络时,中介可以返回之前缓存的页面或资源;
- 页面请求资源时,中介可以按照既定策略选择缓存或网络;
- 网络请求成功后,中介可以更新缓存,供下次访问使用;
- 收到 Push 事件时,中介可以展示通知;
- 在支持 Background Sync 的浏览器中,中介可以在合适的时机重试短时任务。
不过,Service Worker 并不是一个始终运行的后台进程。它空闲时可能被浏览器终止,需要处理 fetch、push、sync 等事件时再重新启动。
注意点
1. 需要安全上下文
Service Worker 正式环境通常必须使用 HTTPS。本地开发时,http://localhost 通常被浏览器视为安全上下文。
正式环境:HTTPS
本地开发:localhost 通常可以使用 HTTP
2. 不能直接操作 DOM
Service Worker 运行在独立的 Worker 上下文中,不能访问:
window;document;- 页面 DOM;
- 页面中的
parent。
它可以访问 self、navigator、location 等 Worker 环境对象,但 Service Worker 中的 location 表示 Service Worker 脚本自身的地址,不是页面当前地址。
3. 通过事件和页面通信
Service Worker 与页面之间通常使用:
postMessage();message事件;clients.matchAll();MessageChannel。
Service Worker 不能直接修改页面内容,需要把数据发送回页面,再由页面脚本操作 DOM。
4. 只能控制作用域内、已受控页面的请求
Service Worker 不是整个浏览器的全局代理。只有位于注册作用域内并且已经被当前 Service Worker 控制的页面,其请求才会触发对应的 fetch 事件。
首次注册后,当前页面通常需要刷新一次,或者由激活后的 Service Worker 使用 clients.claim() 立即接管。
极简化 Demo
下面的 Demo 展示一个最基本的注册、安装、预缓存和运行时缓存流程。
HTML 文件:index.html
页面加载时注册 Service Worker:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Service Worker Demo</title>
</head>
<body>
<h1>Service Worker Demo</h1>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register(
'/service-worker.js'
)
console.log(
'Service Worker registered with scope:',
registration.scope
)
} catch (error) {
console.error('Service Worker registration failed:', error)
}
})
}
</script>
</body>
</html>
navigator.serviceWorker 存在只表示当前环境提供注册接口,注册仍可能因为 HTTPS、脚本路径、作用域或服务器响应错误而失败。
原文的兼容性截图来自较早版本的浏览器数据,只能作为历史参考,具体支持情况应以当前 MDN、Can I Use 或 Baseline 数据为准。

Service Worker 文件:service-worker.js
const CACHE_NAME = 'my-cache-v1'
const PRECACHE_URLS = [
'/',
'/index.html'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(PRECACHE_URLS)
})
)
})
self.addEventListener('fetch', (event) => {
const request = event.request
const url = new URL(request.url)
// Cache Storage 通常只处理 GET 请求
if (request.method !== 'GET') {
return
}
// 这个 Demo 只缓存同源资源
if (url.origin !== self.location.origin) {
return
}
event.respondWith((async () => {
const cache = await caches.open(CACHE_NAME)
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
try {
const networkResponse = await fetch(request)
if (networkResponse.ok) {
event.waitUntil(
cache.put(request, networkResponse.clone())
)
}
return networkResponse
} catch {
return Response.error()
}
})())
})
这个示例采用简单的 Cache First 策略:
- 先读取缓存;
- 有缓存则直接返回;
- 没有缓存则请求网络;
- 网络成功后写入缓存。
生产项目还需要增加离线页面、缓存版本清理、缓存过期、请求分类和更新提示。HTML 通常不建议永久使用 Cache First,否则可能长期读取旧页面。
生命周期和更新
Service Worker 的生命周期通常包括:
installing
↓
installed / waiting
↓
activating
↓
activated
↓
redundant
install
通常用于预缓存资源。传入 event.waitUntil() 的 Promise fulfilled 后,安装才算成功;如果 Promise rejected,安装可能失败。
waiting
如果旧版本仍控制着页面,新版本安装成功后通常会等待旧版本释放客户端。
activate
通常用于清理旧缓存、迁移数据和执行初始化:
const CURRENT_CACHES = new Set([
'my-cache-v1',
'my-runtime-v1'
])
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys()
await Promise.all(
cacheNames
.filter((name) => name.startsWith('my-'))
.filter((name) => !CURRENT_CACHES.has(name))
.map((name) => caches.delete(name))
)
})())
})
只删除自己应用的缓存,不要无条件删除当前源下的所有缓存,因为同一个源可能有多个应用或功能使用 Cache Storage。
skipWaiting() 和 clients.claim()
self.addEventListener('install', () => {
self.skipWaiting()
})
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})
skipWaiting()让新版本跳过 waiting 阶段,尽快进入激活流程;clients.claim()让已激活的 Service Worker 接管当前作用域内尚未受控的页面;clients.claim()不会更新缓存;- 两者可能导致旧页面、新 Service Worker 和新缓存混合运行,应确认版本兼容后再使用。
手动检查更新
const registration = await navigator.serviceWorker.ready
await registration.update()
更新 Service Worker 时应尽量保持脚本 URL 稳定,通过脚本内容变化和缓存版本号完成更新,而不是不断改成 /sw-v2.js、/sw-v3.js。
缓存策略
下面是常见策略:
- Cache First:有缓存时返回缓存,没有时请求并缓存;
- Cache Only:只读取缓存,缓存没有则失败;
- Network First:先请求网络,失败或超时时回退缓存;
- Network Only:不使用 Cache Storage,始终请求网络;
- Stale While Revalidate:有缓存时立即返回,同时请求网络更新缓存,供下一次使用。
Cache Only
async function cacheOnly(request) {
const cache = await caches.open('cache-only-v1')
const response = await cache.match(request)
if (!response) {
throw new Error('Cache miss')
}
return response
}
Network Only
async function networkOnly(request) {
return fetch(request)
}
适合登录、支付和必须获取实时结果的请求。非 GET 请求也通常不应写入 Cache Storage。
Network First
async function networkFirst(request) {
const cache = await caches.open('network-first-v1')
try {
const response = await fetch(request)
if (response.ok) {
await cache.put(request, response.clone())
return response
}
throw new Error(`HTTP ${response.status}`)
} catch {
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
throw new Error('Network and cache are both unavailable')
}
}
fetch() 在收到 HTTP 404 或 500 时通常不会 reject,因此如果希望 HTTP 错误也回退缓存,需要主动检查 response.ok。
Network First with timeout
function fetchWithTimeout(request, timeout = 3000) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeout)
return fetch(request, { signal: controller.signal })
.finally(() => clearTimeout(timer))
}
async function networkFirstWithTimeout(request) {
const cache = await caches.open('network-timeout-v1')
try {
const response = await fetchWithTimeout(request)
if (response.ok) {
await cache.put(request, response.clone())
}
return response
} catch {
const cachedResponse = await cache.match(request)
if (cachedResponse) {
return cachedResponse
}
throw new Error('Network timeout and cache miss')
}
}
超时策略要谨慎使用。请求被 AbortController 取消后,网络请求并不一定在服务器端停止,应用还应处理重试和重复提交问题。
Stale While Revalidate
async function staleWhileRevalidate(event) {
const request = event.request
const cache = await caches.open('stale-v1')
const cachedResponse = await cache.match(request)
const networkPromise = fetch(request).then(async (response) => {
if (response.ok) {
await cache.put(request, response.clone())
}
return response
})
event.waitUntil(networkPromise.catch(() => undefined))
return cachedResponse || networkPromise
}
这种策略适合头像、图片和允许短暂过期的非关键内容。对于余额、订单、权限等数据,不应直接返回旧缓存。
Workbox 百宝箱
Workbox 是一组用于简化 Service Worker 路由和缓存的模块。每个模块负责 Service Worker 开发中的一个具体方面,可以减少重复的底层代码。
workbox-routing
用于匹配请求和路由:
import { registerRoute } from 'workbox-routing'
workbox-strategies
提供常用缓存策略:
CacheFirst;NetworkFirst;StaleWhileRevalidate;NetworkOnly;CacheOnly。
workbox-precaching
用于预缓存构建产物。使用构建工具时,通常由构建流程生成资源清单:
import { precacheAndRoute } from 'workbox-precaching'
precacheAndRoute(self.__WB_MANIFEST)
workbox-expiration
用于限制缓存条目数和缓存时间:
import { ExpirationPlugin } from 'workbox-expiration'
workbox-window
这是在页面窗口上下文中使用的模块,可以帮助注册 Service Worker、监听更新和处理生命周期交互。它不是运行在 Service Worker 内部的缓存策略模块。
Workbox 示例
现代项目应通过 npm 和构建工具使用当前版本 Workbox:
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images-v1',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 7 * 24 * 60 * 60
})
]
})
)
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages-v1',
networkTimeoutSeconds: 3
})
)
Workbox 仍然需要开发者根据资源类型设计缓存策略。它不是自动适配所有项目的黑盒,也不能替代缓存版本、离线页面和错误降级设计。

Service Worker 与 Web Worker 傻傻分不清楚
不少同学使用过 Web Worker。两者都带有 Worker,它们是一回事吗?
相同点
- 都运行在不同于页面主线程的 Worker 执行上下文中;
- 都不能直接访问页面 DOM;
- 都可以通过
postMessage()和message事件通信; - 都不能依赖
window和document操作页面。
“线程池”“进程是否相同”等属于浏览器实现细节,不适合作为规范层面的结论。
不同点
可解决的问题不同
用户 A:
页面操作起来太卡,点击和滚动不流畅。
解决方案:使用 Web Worker,将高计算量任务移出页面主线程。
用户 B:
页面重复打开时加载很慢,或者没有网络时无法访问。
解决方案:使用 Service Worker 控制作用域内的请求、实现缓存和离线回退。
生命周期不同
- Web Worker:通常由页面通过
new Worker()创建,可以由页面调用terminate()销毁; - Service Worker:通过
register()注册,由浏览器按生命周期和事件管理,空闲时可以被终止,需要事件时再启动; - Web Worker 通常只服务于创建它的页面;
- Service Worker 可以控制作用域内的多个页面。
能力不同
| 能力 | Web Worker | Service Worker |
|---|---|---|
| 计算任务 | 适合 | 可以执行事件任务,但主要用途不是计算 |
| 拦截页面请求 | 不负责 | 可以处理受控页面作用域内的 fetch |
| 离线缓存 | 需要自行实现,不能接管页面导航 | 常用能力 |
| Push | 不负责 | 可以监听 push |
| Background Sync | 不负责 | 在支持的浏览器中可以监听 sync |
| DOM | 不能访问 | 不能访问 |
Chrome Extension 的 Service Worker 又是什么?
Chrome Extension Manifest V3 使用 Service Worker 处理扩展的后台逻辑。它与普通网页 Service Worker 都采用 Worker 上下文和事件驱动机制,但运行环境、权限和可用 API 不同。
Chrome Extension Service Worker
主要用于:
- 管理扩展生命周期;
- 处理扩展事件;
- 与弹出页面、选项页、内容脚本通信;
- 使用 Chrome Extension API;
- 保存和管理扩展状态。
扩展 Service Worker 同样可能在空闲时被浏览器终止,不能依赖内存中的全局变量保存持久状态。
普通 Web Service Worker
主要用于:
- 缓存静态资源;
- 提供离线访问;
- 拦截受控页面请求;
- 接收 Web Push;
- 配合 Background Sync。
扩展 Service Worker 与普通 Web Service Worker 不是完全无关,也不是完全相同。它们共享 Worker 的基础执行模型,但属于不同的平台环境。
我能用 Service Worker 做什么?
1. 请求拦截与缓存管理
这是 Service Worker 最常见的用途。可以根据资源类型选择不同策略:
- 静态资源:Cache First;
- HTML:Network First;
- 图片和头像:Stale While Revalidate;
- 登录和支付请求:Network Only;
- 必须离线的应用壳:Cache Only。
2. 推送通知
Push 通知需要完整链路:
- 用户授权通知权限;
- 页面创建 Push subscription;
- 将订阅信息发送到后端;
- 后端通过 Push 服务发送消息;
- Service Worker 监听
push事件并显示通知。
Service Worker 本身不能主动向所有用户发送通知,也不能保证通知一定送达。
3. 后台同步
Background Sync 可以在网络恢复后重试短时、可重试的任务,例如离线消息队列。
self.addEventListener('sync', (event) => {
if (event.tag === 'send-outbox') {
event.waitUntil(flushOutbox())
}
})
Background Sync 不是所有浏览器都支持,也不保证网络恢复后立即执行。应用必须提供页面重新打开后的降级重试方案。
4. 离线支持
PWA 可以通过 Service Worker 缓存应用壳、页面和部分资源,使用户在离线或弱网状态下继续使用部分功能。
离线视频也可以使用缓存,但需要额外考虑:
- 视频文件大小和存储配额;
- Range 请求;
- 浏览器缓存能力;
- 资源版权;
- 用户是否允许占用本地空间。
上手注意事项
1. 调试是个大坑
开发时应熟悉浏览器 DevTools 的 Application 面板,可以查看:
- Service Worker 注册状态;
- 当前作用域;
- waiting 和 active Worker;
- Cache Storage;
- IndexedDB;
- Update on reload;
- Skip waiting;
- Unregister;
- Clear storage。
2. 注销不会自动删除缓存
注销 Service Worker 和删除缓存是两个独立操作:
const registrations = await navigator.serviceWorker.getRegistrations()
await Promise.all(
registrations.map((registration) => registration.unregister())
)
const cacheNames = await caches.keys()
await Promise.all(
cacheNames
.filter((name) => name.startsWith('my-'))
.map((name) => caches.delete(name))
)
不一定必须“在注销前”清除缓存,但如果希望彻底移除应用数据,就必须显式删除相关缓存。
3. 缓存使用不当可能是负优化
缓存并不一定总能提升体验。需要考虑:
- 首次安装预缓存会增加首次加载成本;
- 过大的缓存会占用存储空间;
- Cache First 可能导致内容更新不及时;
- 缓存接口数据可能显示过期信息;
- 错误的缓存策略可能影响 LCP 和页面可用性;
- 关键请求不应被错误的缓存响应替代。
缓存策略应根据业务数据的实时性、离线需求和用户可接受的延迟来设计。
4. 缓存中不要保存敏感数据
不要把密码、访问令牌、支付信息等敏感内容直接写入 Cache Storage。退出登录时也应清理与用户相关的缓存和 IndexedDB 数据。
总结
本文保留了原文中关于 Service Worker、Web Worker、扩展 Service Worker、Workbox 和缓存策略的主要内容,并补充了几个必须掌握的限制:
- Service Worker 需要安全上下文,正式环境通常使用 HTTPS,localhost 可用于本地开发;
- Service Worker 不是永久运行的后台进程,空闲时可能被浏览器终止;
- 只有作用域内、已受控页面的请求才会触发
fetch; install适合预缓存,activate适合清理旧缓存;event.waitUntil()用于等待异步事件任务,event.respondWith()用于提供请求响应;- Web Worker 主要解决计算阻塞,Service Worker 主要处理缓存、离线和后台事件;
- Chrome Extension Service Worker 与普通 Web Service Worker 共享基础模型,但运行环境和权限不同;
- Workbox 可以简化缓存代码,但仍需要正确配置路由、策略、版本和过期时间;
- Push、Background Sync 和离线视频都有浏览器支持、权限、容量和可靠性限制;
- 缓存用得好可以改善重复访问体验,用错则可能造成数据过期、更新延迟和性能下降。