Skip to main content
Service Worker 我用它做过离线缓存:拦截请求容易,难的是缓存策略、更新时机和后台同步这些边界。

Q1: Service Worker 的历史?为什么要有它?

1

Web 早期: 网络是生命线

传统 Web 应用完全依赖网络——断网就是白屏。HTTP 缓存(Cache-Control、ETag 等)只能优化重复请求,无法让应用在离线状态下工作。浏览器能做的只是缓存静态资源,但”缓存”和”离线可用”是两回事。
2

移动时代: Web 体验的致命短板

2012 年前后,移动互联网爆发。用户习惯了原生 App 的”随时可用、推送通知”体验,而 Web 应用做不到——没有网络就打不开,没有推送就唤不回用户。Google 意识到 Web 必须进化。
3

2014-2015 年: Service Worker 与 PWA

2014 年 Service Worker 规范草案发布,Chrome 40(2015 年)开始正式支持。Google 在 2015 年提出 PWA(Progressive Web App)概念,Service Worker 是其核心技术。它是一个运行在浏览器后台的独立线程,可以拦截网络请求、管理缓存、接收推送通知——即使页面关闭,Service Worker 也能被浏览器重新启动。这从根本上解决了 Web 应用的离线问题。
4

现代: 离线优先成为标准

现代浏览器全面支持 Service Worker。大型应用(如 Twitter Lite、Starbucks PWA)已经证明了 PWA 的商业价值——安装包小、加载快、可离线使用。Service Worker 让 Web 应用第一次在体验上接近了原生 App。
Service Worker 解决的核心问题:
Service Worker 是浏览器后台的”网络代理”,让 Web 应用实现了离线缓存、推送通知等原生 App 级别的能力。

🚫 没有 Service Worker

Web 应用就像一个只能在有 WiFi 的房间里才能打开的冰箱——断网了,门都打不开。用户每次访问都要重新下载所有资源,浪费流量、浪费时间。

✅ 引入 Service Worker

有了 Service Worker,Web 应用变成了一个自带食物的冰箱——即使断电(断网),里面的食物(缓存数据)仍然可以使用。而且它还能在后台帮你接收快递(推送通知),即使你不在家(页面关闭)。
回答时先说”Web 的离线痛点”(为什么需要),再引入 Service Worker 的机制(怎么解决)。想听的是你对技术演进必然性的理解,而不是背生命周期。

Q2: Service Worker 的生命周期(install、activate、fetch)

Service Worker 的本质是一个运行在浏览器后台的 JavaScript 线程,它独立于页面,有自己的生命周期。理解这个生命周期是掌握 Service Worker 的关键。
新版本的 Service Worker 安装后,不会立刻激活——它会等到所有使用旧版本的页面都关闭后才激活。这是为了防止新旧版本同时控制页面导致行为不一致。
  • 如果想跳过等待,可以在 install 事件中调用 self.skipWaiting()
  • 新激活的 Service Worker 不会立刻控制已打开的页面,需要刷新页面或使用 clients.claim()
Service Worker 的生命周期就像便利店换店长。新店长(新版本 SW)先来”安装”——熟悉商品摆放(缓存资源)。然后等老店长的最后一班岗结束(旧版本页面全部关闭),新店长才”激活”接手。激活后,新店长开始处理所有顾客请求(拦截 fetch)。如果新店长不想等,可以要求”立即上岗”(skipWaiting)。

页面如何注册 Service Worker

Service Worker 的作用域

Service Worker 的作用域由其文件路径决定。/sw.js 的默认作用域是 /,/assets/sw.js 的默认作用域是 /assets/。可以通过注册时的 scope 选项修改,但不能超出文件路径的范围。

Q3: Cache API 详解(缓存策略、离线支持)

Cache API 是与 Service Worker 配合的核心 API,它提供了一个独立于 HTTP 缓存的键值对存储,可以缓存 Request/Response 对象。

Cache API 的基本操作

五种经典缓存策略

Cache API 的 put 方法需要一个 Response 对象,但 Response 的 body 只能读取一次(ReadableStream 的特性)。所以如果需要同时缓存和返回响应,必须先 clone():
Cache API 就像图书馆的书架。caches.open 是打开一间阅览室,cache.add 是把新书放到书架上,cache.match 是在书架上找书。Stale While Revalidate 策略就像——你先从书架上拿一本旧版书给你看(返回缓存),同时派人去出版社取最新版(发网络请求),取回来后替换掉书架上的旧书(更新缓存)。

Q4: Service Worker 的实际应用(PWA、离线缓存、推送通知)

应用一:PWA 离线应用

应用二:推送通知

应用三:后台同步

应用四:缓存版本管理


Q5: Service Worker 的限制与安全

限制一:只能运行在 HTTPS 上

Service Worker 只能在安全上下文(Secure Context)中运行——即 HTTPS 或 localhost。这是因为 Service Worker 拦截的是网络请求,如果通过 HTTP 传输,中间人可以篡改 Service Worker 脚本,从而控制整个应用的所有网络请求。

限制二:无法访问 DOM

Service Worker 运行在独立的 worker 线程中,无法直接访问 DOM。它与页面的通信只能通过 postMessage API。

限制三:无同步 XHR

Service Worker 中不能使用同步 XMLHttpRequest。所有网络请求必须是异步的(使用 Fetch API)。这是为了保证 Service Worker 不会阻塞主线程。

限制四:不能访问 localStorage

Service Worker 无法使用 localStorage(因为 localStorage 与 window 绑定)。可以使用 IndexedDB 或 Cache API 作为替代。

限制五:浏览器兼容性与生命周期管理

Service Worker 在某些场景下会被浏览器终止以节省内存(尤其是移动端)。虽然浏览器会在需要时重新启动它,但开发者不能假设 Service Worker 一直在运行。所有状态必须持久化到 IndexedDB 或 Cache API 中。

Q6: 经典面试题:Service Worker 和 Web Worker 的区别?

关键区别: ❌/✅ 代码对比(通信方式):
Service Worker 用于需要离线能力、推送通知、后台同步的场景(PWA)。Web Worker 用于把计算密集型任务从主线程剥离的场景(大数组排序、图片处理等)。两者解决的是完全不同的问题。

底层源码:Chromium 是如何实现 Service Worker 的?

Chromium 的 Service Worker 实现在 content/browser/service_worker/ 目录下。核心类是 ServiceWorkerVersion(详见 Chromium 源码:content/browser/service_worker/service_worker_version.h):
这段代码做了 3 件事:管理 Service Worker 的 6 种生命周期状态、将 fetch 请求路由到正确的 worker 线程、处理 install/activate/fetch 事件的调度。 Fetch 拦截在 Blink 渲染引擎的 ServiceWorkerGlobalScope 中实现(详见 Chromium 源码:third_party/blink/renderer/modules/service_worker/):

及格回答 vs 满分回答

问题:“说说 Service Worker 的生命周期”

及格回答(60-70分):
“Service Worker 有三个生命周期:install 缓存资源,activate 清理旧缓存,fetch 拦截请求。”
问题出在:知道三个事件但不理解激活等待机制。 满分回答(90-100分):
Service Worker 的生命周期包含注册(register)、安装(install)、激活(activate)和运行(fetch)四个阶段。注册由页面 JS 触发,浏览器在后台下载并解析 Service Worker 脚本。install 事件用于预缓存关键静态资源——通过 caches.open() 和 cache.addAll() 将资源存入 Cache API。event.waitUntil() 可以延长 install 阶段,确保所有资源缓存完成。[1]
关键的细节是激活等待机制:新版本 Service Worker 安装完成后不会立刻激活,它会等到所有使用旧版本的页面都关闭后才进入 activate 阶段。这是为了防止新旧版本同时控制页面导致行为不一致。如果需要跳过等待,可以在 install 中调用 self.skipWaiting(),在 activate 中调用 self.clients.claim() 立即接管所有页面。[2]
activate 事件的核心任务是清理旧缓存——通过 caches.keys() 获取所有缓存名称,删除不在白名单中的旧缓存。这保证了用户的存储空间不会被无限占用。
实际项目中需要注意:浏览器只在特定时机检查更新——用户导航到作用域内的页面、调用 navigator.serviceWorker.register(),或浏览器自身的定期检查(规范建议约 24 小时一次)。因此新版本往往不会在发版后立刻生效。想提示用户”有新版本可用”,应该在 registration.onupdatefound 或新 Worker 进入 waiting 状态时通过 postMessage 通知,而不是等 activate 之后——那时新版本已经在接管了。

常见追问

答题要点: 从生命周期、能力范围、使用场景三个维度对比。高分回答: “Service Worker 和 Web Worker 都是运行在独立线程中的 JS 脚本,但它们的定位完全不同。 生命周期:Web Worker 依附于页面,页面关闭即终止;Service Worker 独立于页面,可以后台存活(即使页面关闭,SW 仍在运行,直到浏览器决定终止它以节省内存)。 能力范围:Service Worker 可以拦截网络请求、管理缓存、接收推送通知;Web Worker 只能做计算。 使用场景:Service Worker 用于 PWA 离线缓存、推送通知、后台同步;Web Worker 用于计算密集型任务(大数组排序、图片处理)。
答题要点: 不只是列出策略,要给出选择依据。高分回答: “五种经典缓存策略:
  1. Cache First:缓存优先,适用于静态资源(CSS、JS、图片)——这些资源更新频率低,缓存命中率高。
  2. Network First:网络优先,适用于需要最新数据的页面(新闻、API)——网络失败时降级到缓存。
  3. Stale While Revalidate:先返回缓存,同时后台更新,适用于频繁更新但允许稍旧的数据(用户头像、评论列表)。
  4. Cache Only:只走缓存,适用于预缓存的离线资源。
  5. Network Only:只走网络,适用于不能缓存的请求(登录、支付、实时数据)。
选择依据:数据时效性和离线需求。不常变的用 Cache First,常变的用 Network First 或 Stale While Revalidate。”
答题要点: 缓存版本管理 + 激活清理 + 用户通知。高分回答: “更新缓存的核心是版本管理:
浏览器的更新检测:当 SW 文件内容发生字节级变化时,浏览器会触发更新流程。所以很多项目会在构建时给 SW 文件名加 hash。”
答题要点: 区分”首次访问”和”二次访问”两种场景。高分回答: “分两种情况: 1. 首次访问(从未缓存过):Service Worker 需要在 install 阶段预缓存关键资源。如果首次访问时就断网,预缓存会失败——所以 PWA 的首次加载仍然需要网络。 2. 二次访问(已有缓存):Service Worker 可以从 Cache API 返回缓存的响应,即使网络不可用。这就是离线能力的核心。
关键点:要准备一个 offline.html 作为兜底页面,当请求和缓存都没有时返回给用户。”
答题要点: 推送通知需要三方配合:Push API、Notification API、Service Worker。高分回答: “推送通知需要三个 API 配合:
  1. Push API(PushManager):订阅推送,获取推送端点(endpoint)
  2. Notification API:显示系统通知
  3. Service Worker:接收推送事件,显示通知
服务器通过 VAPID 协议向浏览器推送端点发送加密消息,浏览器收到后触发 Service Worker 的 push 事件。”
答题要点: 作用域决定了 SW 能控制哪些页面。高分回答: “Service Worker 的作用域由其文件路径和注册时的 scope 选项共同决定。默认作用域是 SW 文件所在的目录。例如 /assets/sw.js 的默认作用域是 /assets/。 关键规则:
  1. scope 不能超出 SW 文件的目录范围——/assets/sw.js 不能注册 / 的作用域
  2. 一个页面只能被一个 SW 控制——如果多个 SW 的作用域重叠,最具体的(最长路径匹配的)获胜
  3. 顶层作用域(/)的 SW 可以控制所有页面
最佳实践:把 SW 放在根目录,控制整个站点,通过 fetch 事件中的 URL 判断不同的缓存策略。”
答题要点: 浏览器的更新检测逻辑 + 强制更新方法。高分回答: “浏览器检测 Service Worker 更新的条件(满足任一即可):
  1. 用户导航到 SW 作用域内的新页面
  2. 调用 navigator.serviceWorker.register()(即使路径相同)
  3. 距离上次检查超过 24 小时(浏览器自动检查)
更新检测流程:浏览器重新下载 SW 文件,与当前版本逐字节比较。如果内容不同,触发新版本的 install 流程。强制更新的方法:
注意:skipWaiting() 会导致新旧版本同时控制页面,可能导致状态不一致。建议在 activate 后通知用户刷新页面。”
答题要点: Background Sync API 的使用场景和限制。高分回答: “后台同步(Background Sync)允许 Service Worker 在网络恢复时自动发送离线期间积累的数据。典型场景:离线表单提交、离线消息发送。
限制:后台同步不保证精确时间——浏览器会根据电量、网络状况等决定何时触发。不要用它做定时任务。“

延伸阅读与引用参考