前端一面:Web Worker 与 Service Worker——多线程与离线缓存核心考点

浏览器里的「Worker」相关问题,在一面中经常以「多线程」「离线缓存」「消息通道」这些关键词出现。
这一篇试着把常见考点串成一条线:浏览器为什么需要 Worker、多种 Worker 各自干什么,以及在面试里容易被混淆的边界有哪些。

可以先回到一个老问题:为什么在主线程里写一个死循环,整个页面就卡死了?

  • 浏览器的大部分前端代码(JS)、DOM 渲染、事件处理都在同一个主线程上运行。
  • 如果在主线程里执行一个耗时很长的任务(如大循环、复杂计算),这个线程就被一直占用。
  • 线程被占用期间:
    • 用户事件(点击、滚动)得不到及时处理;
    • DOM 不能顺利更新、页面看起来就「假死」。

Worker 相关能力,核心目标就是两件事:

  • 把重计算挪到主线程之外(Web Worker 方向);
  • 把网络层/缓存能力下放到更靠近浏览器底层的一层,为离线和加速铺路(Service Worker 方向)。

理解这一点,再往下看各种 Worker,会更容易分清它们各自解决的是什么问题。

可以先给一个简单概括:

  • Web Worker 是浏览器提供的一种在后台线程中运行 JS 的机制。
  • 它有自己独立的执行环境,不能直接操作 DOM
  • 和主线程之间通过消息传递(postMessage + onmessage)交换数据。

几个关键特性:

  • 多线程,但不是共享内存的多线程模型
    • 传统浏览器环境中,每个 Worker 有自己独立的 JS 运行环境。
    • 主线程和 Worker 通过消息拷贝或结构化克隆数据,而不是随意访问同一块内存。
  • 没有 DOM / BOM 直接访问能力
    • 在 Worker 里不能直接操作 documentwindow,这也是很多一面题最爱考的点。
  • 适合做什么?
    • 大量 CPU 计算:比如复杂算法、数据处理、长轮询心跳处理等。
    • 不适合直接参与 UI 渲染,只负责算结果,然后把结果发回主线程。

最经典的使用方式可以抽象成三步:

  • 主线程创建 Worker:
1const worker = new Worker('worker.js');
  • 主线程向 Worker 发送数据:
1worker.postMessage({ type: 'START', payload: bigData });
  • Worker 里接收消息、处理并回传结果:
1// worker.js
2self.onmessage = (event) => {
3  const { type, payload } = event.data;
4  if (type === 'START') {
5    const result = heavyCompute(payload);
6    self.postMessage({ type: 'DONE', payload: result });
7  }
8};

在面试里,如果被问到「Web Worker 能解决什么问题」,与其只说「能多线程」,不如直接用一句话回答:

  • 把会阻塞主线程的重计算,丢到 Worker 里去做,再通过消息把结果发回来,从而避免页面卡顿。

相比 Web Worker,Service Worker 更偏向「网络层」:

  • 运行在浏览器和 Web 应用之间的一层脚本。
  • 可以拦截页面发起的请求,读写自己的缓存,决定返回网络数据还是本地缓存。
  • 支持离线访问、资源预缓存、PWA、推送通知等能力。

一个有用的心智模型是:

  • 可以把 Service Worker 理解为:挂在浏览器里的一个「轻量代理层」
  • 它不直接渲染 UI,但能决定页面拿到的数据来自哪里(网络还是缓存)。
  • 有自己的生命周期,与页面不同步
    • 安装(install)→ 激活(activate)→ 工作中(fetch 等事件)。
    • 即使所有页面都关闭,只要浏览器认为有必要,Service Worker 仍可能在后台短暂运行。
  • 只能在 HTTPS 或 localhost 环境下工作
    • 因为它可以拦截请求并改写响应,安全要求更高。
  • 主要能力集中在两个方面
    • 拦截 fetch 事件,实现离线缓存策略(Cache First / Network First 等)。
    • 处理推送通知、后台同步等。

在一面场景中,Service Worker 常见问法包括:

  • 「怎么实现一个简单的离线缓存页面?」
  • 「Service Worker 和浏览器缓存(HTTP 缓存、localStorage)有什么区别?」

这时候,只要能清楚地描述出它站在网络请求路径上的位置,基本就能答到点上。

很多同学在面试里一听到「Worker」,就把 Web Worker 和 Service Worker 混在一起。可以从三个维度去拆开:

  • 解决的问题不一样
    • Web Worker:解决的是 主线程被重计算阻塞 的问题。
    • Service Worker:解决的是 网络不稳定 / 需要离线缓存 / 需要更灵活缓存策略 的问题。
  • 运行位置和能力不同
    • Web Worker:
      • 和页面 JS 同属前端逻辑的一部分,只是换了一个线程执行。
      • 不能拦截网络请求,主要做计算。
    • Service Worker:
      • 更靠近网络层,可以拦截页面发出的请求。
      • 可以访问专门的缓存存储(Cache Storage),实现自定义缓存策略。
  • 生命周期和触发方式不同
    • Web Worker:
      • 由页面代码显式创建和销毁,随页面生命周期结束而结束。
    • Service Worker:
      • 安装后由浏览器托管,生命周期相对独立,可以在没有页面时被唤醒处理事件。

在一面作答时,可以用一句小结来区分:

  • Web Worker 偏「算」,Service Worker 偏「网」。一个负责把计算挪出主线程,一个负责在网络和页面之间加一层可编程缓存与代理。

结合前面的概念,可以把高频题目归类并各给一个清晰的答题思路。

答题时可以分三步:

  • 先描述问题场景:
    • 「比如有一个需要处理大量数据的计算任务,在主线程里跑会造成明显卡顿。」
  • 再讲思路:
    • 「可以把这段重计算逻辑单独抽到 Worker 里运行,通过 postMessage 把输入数据发给 Worker,算完再把结果发回来。」
  • 最后补一句注意事项:
    • 「Worker 里不能直接操作 DOM,只负责计算结果;同时注意传输数据的体积和频率,避免过大的消息开销。」

如果有时间,可以再顺带提到:

  • 有些情况下可以用多个 Worker 做并行切分,但要注意浏览器线程数量上限以及创建/销毁开销。

可以抓住这几个点:

  • HTTP 缓存更多是由服务器响应头和浏览器策略控制,粒度较粗。
  • Service Worker 提供的是 可编程的缓存控制
    • 可以在 install 阶段预缓存一些关键资源。
    • fetch 事件中按规则决定:先查缓存还是先走网络,或者网络失败时才回退到缓存。

示例式回答可以是:

  • 「我会写一个简单的 Service Worker,在 install 事件里用 Cache API 把核心静态资源缓存起来,在 fetch 事件里先尝试从缓存读,如果没有再走网络,这样用户在离线时仍然可以打开页面的基本框架。」

这一题最容易被问得很宽,答题时可以先区分 Web Worker 和 Service Worker,再给出各自的边界:

  • Web Worker 能做:
    • 复杂计算、解析、编码转换等 CPU 密集型任务。
    • 通过消息和主线程配合完成某些业务流程。
  • Web Worker 做不到:
    • 直接访问 DOM、windowdocument
    • 参与渲染流程(布局、绘制)。
  • Service Worker 能做:
    • 拦截网络请求,读写专门的缓存。
    • 支持离线访问、消息推送等。
  • Service Worker 做不到:
    • 直接操作页面 DOM。
    • 随意长时间常驻内存(由浏览器调度,可能被回收,不能当长期常驻的后端服务来用)。

围绕 Worker 相关的问题,答题可以沿着这样一条主线展开:

  • 先说「为什么需要它」——浏览器单线程 + 页面卡顿 / 离线访问需求。
  • 再说「它具体解决了什么问题」——Web Worker 解决重计算阻塞,Service Worker 解决网络与缓存控制。
  • 最后补充「能干什么 / 不能干什么」和「常见使用场景」。

只要把这三个层次说清楚,即使不展开所有 API 细节,也足以覆盖绝大多数一面里和 Worker 相关的考点。