一、为什么前端需要 Workers
浏览器中的 JavaScript 默认运行在主线程。主线程不仅负责执行 JS,还负责页面渲染、样式计算、布局、绘制、事件响应等工作。如果某段 JS 长时间占用主线程,页面就会出现卡顿、点击无响应、动画掉帧等问题。
Workers 的出现,是为了把部分任务从主线程中拆出去,让浏览器可以更流畅地处理用户交互和页面渲染。
常见 Worker 类型包括:
- Web Worker:把计算任务放到后台线程执行。
- Dedicated Worker:专属于一个页面或脚本的 Worker,最常见。
- Shared Worker:可被同源多个页面共享的 Worker。
- Service Worker:运行在浏览器和网络之间的代理层,常用于缓存、离线、PWA、推送。
- Worklet:更轻量的专用工作线程,例如 AudioWorklet、PaintWorklet。
二、Web Worker 和 Service Worker 的核心区别
Web Worker 和 Service Worker 都能在主线程之外运行 JavaScript,但它们的定位完全不同。
| 对比项 | Web Worker | Service Worker |
|---|---|---|
| 核心用途 | 后台计算、多线程任务 | 网络代理、缓存、离线、PWA |
| 生命周期 | 通常由页面创建和销毁 | 独立于页面,有安装、激活、终止等生命周期 |
| 是否能访问 DOM | 不能 | 不能 |
| 是否能拦截请求 | 不能 | 可以拦截受控页面的网络请求 |
| 通信方式 | postMessage |
postMessage、clients、事件机制 |
| 使用限制 | 同源脚本或可访问脚本 | 通常要求 HTTPS 或 localhost |
| 典型场景 | 大数据计算、图片处理、加密压缩 | 离线访问、静态资源缓存、消息推送 |
一句话概括:Web Worker 解决"计算别卡主线程",Service Worker 解决"网络和缓存可控"。
三、浏览器线程模型基础
主线程是前端最关键的执行线程,页面大部分工作都发生在主线程上。
主线程常见任务:
- 执行 JavaScript。
- 解析 HTML。
- 计算 CSS 样式。
- 执行布局 Layout。
- 执行绘制 Paint。
- 处理输入事件。
- 执行动画回调。
如果 JavaScript 长时间执行,浏览器无法及时渲染和响应用户输入。
Web Worker 的价值就是把耗时 JS 任务从主线程移到后台线程。主线程只负责发送任务、接收结果和更新 UI。
四、Web Worker 基础用法
1. 创建 Worker
主线程代码:
js
const worker = new Worker('/worker.js');
worker.postMessage({ type: 'sum', payload: [1, 2, 3] });
worker.onmessage = event => {
console.log('计算结果:', event.data);
};
worker.onerror = error => {
console.error('Worker 出错:', error);
};
Worker 文件 worker.js:
js
self.onmessage = event => {
const { type, payload } = event.data;
if (type === 'sum') {
const result = payload.reduce((total, item) => total + item, 0);
self.postMessage(result);
}
};
2. Worker 通信模型
主线程和 Worker 之间不能直接共享普通对象,它们主要通过消息通信。
3. 终止 Worker
主线程可以主动终止 Worker:
js
worker.terminate();
Worker 内部也可以自己关闭:
js
self.close();
长期不用的 Worker 应及时释放,避免占用内存和线程资源。
五、Web Worker 的运行环境
Worker 不是浏览器窗口环境,它没有完整的 DOM API。
Worker 中不能使用:
documentwindow作为页面窗口对象- DOM 查询和操作
- 直接修改页面 UI
- 部分浏览器 UI API
Worker 中可以使用:
selfsetTimeout、setIntervalfetchXMLHttpRequestPromiseWebSocketIndexedDBcryptoimportScripts,经典 Worker 中可用
js
self.addEventListener('message', event => {
fetch('/api/data')
.then(response => response.json())
.then(data => self.postMessage(data));
});
注意:Worker 能发起请求,但不能像 Service Worker 一样拦截页面请求。
六、Worker 的数据传递机制
1. 结构化克隆
postMessage 默认使用结构化克隆算法复制数据。它可以传递对象、数组、Map、Set、Blob、File、ArrayBuffer 等。
js
worker.postMessage({
list: [1, 2, 3],
meta: new Map([['source', 'main']])
});
结构化克隆不是 JSON 序列化,它支持更多数据类型,但函数、DOM 节点等不能被克隆。
2. Transferable 对象
大数据传递时,复制成本可能很高。可以使用 Transferable 把数据所有权转移给 Worker。
js
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage(buffer, [buffer]);
转移后,主线程中的 buffer 会变为不可用。这样可以避免大块内存复制。
3. SharedArrayBuffer
SharedArrayBuffer 可以让多个线程共享同一块内存,配合 Atomics 做同步控制。
js
const shared = new SharedArrayBuffer(4);
const view = new Int32Array(shared);
worker.postMessage(shared);
Atomics.store(view, 0, 1);
它适合高性能场景,但对安全环境要求更高,通常需要跨源隔离相关响应头。
七、Web Worker 的典型应用
1. 大数据计算
例如前端需要对几十万条数据进行筛选、聚合、排序、统计,如果直接在主线程执行,很容易卡住页面。
js
// main.js
const worker = new Worker('/data-worker.js');
worker.postMessage({ type: 'aggregate', list: bigList });
worker.onmessage = event => {
renderChart(event.data);
};
js
// data-worker.js
self.onmessage = event => {
const { list } = event.data;
const result = list.reduce((map, item) => {
map[item.type] = (map[item.type] || 0) + item.value;
return map;
}, {});
self.postMessage(result);
};
2. 图片处理
图片压缩、滤镜、裁剪、像素分析都可能消耗大量 CPU。可以结合 Worker 和 OffscreenCanvas。
js
const canvas = document.querySelector('canvas');
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
Worker 中处理:
js
self.onmessage = event => {
const { canvas } = event.data;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
};
3. 文件解析
Excel、CSV、日志文件、大 JSON 文件都可以在 Worker 中解析,避免主线程卡顿。
4. 加密、压缩和解压
哈希计算、签名、压缩、解压等 CPU 密集型任务适合放入 Worker。
5. 前端搜索和索引
本地全文检索、模糊搜索、索引构建可以放到 Worker 中,主线程只负责展示结果。
6. WebAssembly 配合 Worker
高性能计算模块可以用 WebAssembly 运行在 Worker 中,例如音视频处理、图像算法、CAD、游戏逻辑等。
八、Web Worker 的工程化使用
1. Vite 中创建 Worker
js
const worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
2. Webpack 5 中创建 Worker
js
const worker = new Worker(new URL('./worker.js', import.meta.url));
3. TypeScript 中使用 Worker
可以给消息定义明确类型:
ts
type WorkerRequest =
| { type: 'sum'; payload: number[] }
| { type: 'sort'; payload: number[] };
type WorkerResponse =
| { type: 'sumResult'; payload: number }
| { type: 'sortResult'; payload: number[] };
这样主线程和 Worker 之间的协议更稳定。
4. 封装 Promise 风格调用
js
function callWorker(worker, payload) {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
function handleMessage(event) {
if (event.data.id === id) {
worker.removeEventListener('message', handleMessage);
resolve(event.data.result);
}
}
worker.addEventListener('message', handleMessage);
worker.addEventListener('error', reject, { once: true });
worker.postMessage({ id, payload });
});
}
复杂项目中建议定义统一消息协议、错误协议和超时机制。
九、Web Worker 的限制和注意事项
1. 不要把所有任务都丢给 Worker
Worker 创建和通信都有成本。如果任务很轻,放进 Worker 反而可能更慢。
2. 注意大对象复制成本
大对象频繁 postMessage 可能带来明显性能开销。二进制大数据优先考虑 Transferable。
3. Worker 不能直接操作 DOM
Worker 计算完结果后,必须通知主线程,由主线程更新页面。
4. 注意错误处理
Worker 报错不会像普通代码一样直接显示在主线程调用栈中,应监听 error 和 messageerror。
js
worker.addEventListener('error', error => {
console.error(error.message, error.filename, error.lineno);
});
worker.addEventListener('messageerror', error => {
console.error('消息反序列化失败', error);
});
5. 注意资源释放
页面卸载、组件销毁、任务完成后,应根据场景调用 terminate()。
十、Shared Worker 简介
Shared Worker 可以被同源下多个页面、iframe 或脚本共享。它适合跨标签页共享连接、共享状态、协调任务。
主页面:
js
const sharedWorker = new SharedWorker('/shared-worker.js');
sharedWorker.port.start();
sharedWorker.port.postMessage({ type: 'connect' });
sharedWorker.port.onmessage = event => {
console.log(event.data);
};
Shared Worker:
js
const ports = [];
self.onconnect = event => {
const port = event.ports[0];
ports.push(port);
port.onmessage = messageEvent => {
ports.forEach(item => {
item.postMessage(messageEvent.data);
});
};
};
典型应用:
- 多标签页共享 WebSocket。
- 多页面共享缓存状态。
- 多窗口消息广播。
- 减少重复后台任务。
十一、Service Worker 是什么
Service Worker 是一种特殊 Worker。它运行在浏览器后台,位于页面和网络之间,可以拦截请求、读取缓存、返回自定义响应。
它是 PWA 的核心技术之一。
Service Worker 的关键特征:
- 不能直接访问 DOM。
- 可以拦截受控页面的网络请求。
- 可以使用 Cache Storage。
- 可以在页面关闭后被浏览器唤醒处理事件。
- 生命周期由浏览器管理,不由页面完全控制。
- 通常必须运行在 HTTPS 或 localhost 环境下。
十二、注册 Service Worker
页面中注册:
js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker 注册成功', registration.scope);
} catch (error) {
console.error('Service Worker 注册失败', error);
}
});
}
scope 决定 Service Worker 能控制哪些路径下的页面。默认情况下,/sw.js 可以控制整个站点,/assets/sw.js 通常只能控制 /assets/ 路径下的资源。
十三、Service Worker 生命周期
Service Worker 的生命周期比普通脚本复杂,主要包括注册、安装、激活、控制页面、空闲终止、事件唤醒。
1. install 阶段
通常用于预缓存关键资源。
js
const CACHE_NAME = 'app-cache-v1';
const PRE_CACHE = ['/', '/index.html', '/styles.css', '/main.js'];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRE_CACHE))
);
});
2. activate 阶段
通常用于清理旧缓存。
js
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
);
})
);
});
3. fetch 阶段
用于拦截请求并返回缓存或网络响应。
js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
十四、Service Worker 更新机制
Service Worker 更新经常让开发者困惑。浏览器会定期检查 sw.js 是否变化。如果发现文件内容变化,会安装新版本。
但是新版本不会总是立即接管页面。默认行为是:
- 新 Service Worker 下载并安装。
- 如果旧页面仍被旧 Service Worker 控制,新版本进入 waiting 状态。
- 等所有旧页面关闭后,新版本才激活。
- 新版本激活后控制后续页面。
如果希望新版本尽快生效,可以使用:
js
self.addEventListener('install', event => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
});
但要谨慎使用,因为新旧资源混用可能导致页面异常。生产环境通常需要结合版本提示,让用户刷新。
十五、Cache Storage 基础
Service Worker 常用 Cache Storage 管理请求和响应。
js
const cache = await caches.open('app-cache-v1');
await cache.put('/api/user', response.clone());
const cached = await cache.match('/api/user');
注意事项:
Response流只能读取一次,缓存前通常需要response.clone()。- Cache Storage 不会自动过期,需要自己管理版本或数量。
- 不要缓存包含敏感信息的私有响应,除非确认安全策略。
- POST 请求默认不适合作为普通静态缓存键。
十六、常见缓存策略
1. Cache First
先读缓存,缓存没有再请求网络。适合版本化静态资源。
js
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
流程:
2. Network First
先请求网络,失败时回退缓存。适合 HTML、接口数据等需要新鲜度的资源。
js
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open('runtime-cache');
cache.put(request, response.clone());
return response;
} catch (error) {
return caches.match(request);
}
}
3. Stale While Revalidate
先返回缓存,同时后台请求网络更新缓存。适合兼顾速度和新鲜度的资源。
js
async function staleWhileRevalidate(request) {
const cache = await caches.open('runtime-cache');
const cached = await cache.match(request);
const networkPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached || networkPromise;
}
4. Network Only
完全走网络,不缓存。适合支付、下单、敏感接口等。
5. Cache Only
只读缓存,不请求网络。适合离线包内固定资源。
十七、Service Worker 的典型应用
1. 离线访问
预缓存 HTML、CSS、JS、图片和离线兜底页,让用户在弱网或断网时仍能打开基础页面。
js
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
}
});
2. PWA 应用
Service Worker 配合 Web App Manifest,可以实现类似原生应用的体验:
- 添加到桌面。
- 离线可用。
- 启动页和图标。
- 后台同步。
- 消息推送。
3. 静态资源加速
对带 hash 的静态资源使用 Cache First,可以减少重复下载。
4. 接口缓存
对部分低频变化、可容忍短暂过期的数据使用 Network First 或 Stale While Revalidate。
5. 请求降级和兜底
网络失败时返回缓存、默认数据、兜底图或离线页面,提高弱网体验。
6. Push 推送
Service Worker 可以接收 Push 事件并展示通知。
js
self.addEventListener('push', event => {
const data = event.data ? event.data.json() : { title: '新消息' };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body || '你有一条新通知'
})
);
});
7. Background Sync 后台同步
用户离线提交数据时,可以先存入 IndexedDB,等网络恢复后由 Service Worker 同步。
十八、Service Worker 安全注意事项
1. 必须使用 HTTPS
Service Worker 能拦截请求,权限很高,因此浏览器要求它运行在安全上下文中。开发环境 localhost 通常被允许。
2. 谨慎设置作用域
scope 越大,Service Worker 能控制的页面越多。不要让不相关路径被错误控制。
3. 避免缓存敏感数据
用户隐私、鉴权响应、个人接口数据如果进入 Cache Storage,可能带来安全和合规风险。
4. 防止缓存投毒
不要无条件缓存所有网络响应,应检查状态码、请求方法、资源类型、来源域名。
js
if (request.method === 'GET' && response.ok && request.url.startsWith(location.origin)) {
cache.put(request, response.clone());
}
5. 注意登出清理缓存
用户退出登录时,应按业务要求清理用户相关缓存,避免下一个用户看到前一个用户的数据。
6. 避免无限缓存增长
Cache Storage 不会自动帮业务做精细淘汰,长期运行可能膨胀,需要版本管理、最大数量限制或过期策略。
十九、Web Worker 与 Service Worker 通信
页面可以和 Service Worker 通信:
js
navigator.serviceWorker.controller?.postMessage({ type: 'PING' });
navigator.serviceWorker.addEventListener('message', event => {
console.log('来自 Service Worker:', event.data);
});
Service Worker 回复页面:
js
self.addEventListener('message', event => {
event.source.postMessage({ type: 'PONG' });
});
也可以向所有受控页面广播:
js
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'CACHE_UPDATED' });
});
});
Web Worker 和 Service Worker 一般不直接互相通信,常见方式是通过页面中转,或者通过 IndexedDB、BroadcastChannel 等共享机制协作。
二十、BroadcastChannel 跨上下文通信
BroadcastChannel 可以让同源下的多个页面、iframe、Worker、Service Worker 进行广播通信。
页面中:
js
const channel = new BroadcastChannel('app-channel');
channel.postMessage({ type: 'USER_LOGOUT' });
channel.onmessage = event => {
console.log(event.data);
};
Worker 或 Service Worker 中也可以创建同名频道监听消息。
适合场景:
- 多标签页同步登录状态。
- 通知缓存更新。
- 多页面共享任务进度。
- 页面和 Worker 之间解耦通信。
二十一、两者组合的实际架构
一个复杂前端应用可能同时使用 Web Worker 和 Service Worker:
- Service Worker 负责静态资源缓存、离线兜底、接口缓存。
- Web Worker 负责大数据计算、文件解析、图像处理。
- 页面主线程负责 UI 渲染和用户交互。
- IndexedDB 负责跨线程持久化数据。
这种架构适合:
- 离线优先应用。
- 大型数据看板。
- 在线文档编辑器。
- 图片、音视频处理工具。
- 本地优先的前端数据库应用。
- PWA 移动 Web 应用。
二十二、性能优化建议
1. 识别长任务
使用 Performance 面板观察主线程长任务。如果某段 JS 超过 50ms,就可能影响用户交互。
2. 只把合适任务放入 Worker
适合 Worker 的任务通常具备:计算量大、可异步、输入输出清晰、不依赖 DOM。
3. 控制通信频率
不要高频发送大量小消息,可以批处理、节流或使用共享内存。
4. 使用 Transferable 优化大数据传输
图片、音视频、二进制文件优先使用 ArrayBuffer 转移所有权。
5. 合理设计缓存策略
Service Worker 缓存不是越多越好,应按资源类型选择策略。
6. 监控缓存命中率和错误率
缓存策略上线后,需要观察资源加载失败、白屏、版本不一致等问题。
二十三、常见问题排查
1. Web Worker 加载失败
检查:
- Worker 文件路径是否正确。
- 是否满足同源要求。
- 构建工具是否正确处理 Worker 文件。
- MIME 类型是否正确。
- 是否使用了当前浏览器不支持的模块语法。
2. Worker 中访问 DOM 报错
这是正常限制。应在 Worker 中计算,在主线程中更新 DOM。
3. Service Worker 注册成功但不拦截请求
检查:
- 页面是否在 Service Worker 的 scope 下。
- 当前页面是否已经被 Service Worker 控制。
- 是否需要刷新一次页面。
- fetch 事件是否正确
respondWith。 - 是否在 HTTPS 或 localhost 下运行。
4. Service Worker 更新不生效
检查:
sw.js内容是否真的变化。- 新 Worker 是否处于 waiting 状态。
- 是否有旧页面未关闭。
- 是否需要提示用户刷新。
- 是否错误缓存了
sw.js本身。
5. 页面出现新旧资源混用
常见原因是 HTML 使用新版本,但 JS/CSS 被旧缓存命中,或反过来。建议静态资源文件名带 hash,HTML 使用 Network First 或不强缓存。
二十四、最佳实践清单
Web Worker 最佳实践
- 只处理 CPU 密集或大数据任务。
- 建立清晰的消息协议。
- 给请求加唯一 ID,方便异步响应匹配。
- 大数据使用 Transferable。
- 监听
error和messageerror。 - 页面销毁时及时
terminate()。 - 不要在 Worker 中依赖 DOM 和浏览器 UI。
Service Worker 最佳实践
- 只在 HTTPS 或 localhost 下启用。
- 明确 scope 范围。
- 不缓存敏感接口和不安全响应。
- HTML、静态资源、接口使用不同缓存策略。
- 版本升级时清理旧缓存。
- 对更新流程做用户提示。
- 登出时清理用户相关缓存。
- 避免缓存
sw.js本身导致更新异常。
二十五、面试常见问题
1. Web Worker 为什么不能操作 DOM
因为 DOM 主要由主线程管理。如果多个线程同时直接修改 DOM,会带来复杂的并发一致性问题。Worker 通过消息把计算结果交给主线程,由主线程统一更新 UI。
2. Web Worker 能不能提升所有 JS 性能
不能。Worker 有创建成本和通信成本,只适合计算量较大、可异步拆分的任务。轻量逻辑放进 Worker 可能得不偿失。
3. Service Worker 和浏览器 HTTP 缓存有什么区别
HTTP 缓存由浏览器根据响应头自动管理,Service Worker 缓存由业务代码控制。Service Worker 可以决定缓存策略、离线兜底和自定义响应,但也需要开发者负责更新和清理。
4. Service Worker 为什么要求 HTTPS
因为它能拦截和改写请求响应。如果在不安全网络中被篡改,会造成严重安全风险。HTTPS 可以保证 Service Worker 脚本来源可信。
5. Service Worker 更新为什么不是立即生效
为了避免正在运行的页面被新脚本突然接管导致状态不一致,浏览器默认让新版本等待旧页面关闭后再激活。
6. PWA 一定要 Service Worker 吗
严格来说,完整 PWA 能力通常离不开 Service Worker,尤其是离线缓存、请求代理、后台同步、推送通知等能力。
二十六、总结
Web Worker 和 Service Worker 都是现代前端非常重要的后台能力,但它们解决的问题不同。
Web Worker 的重点是性能:
- 把耗时计算移出主线程。
- 避免页面卡顿。
- 适合大数据、文件、图片、加密、WASM 等任务。
Service Worker 的重点是网络和离线:
- 拦截请求。
- 管理缓存。
- 支持离线访问。
- 支撑 PWA、推送、后台同步等能力。
选择时可以记住:
在实际工程中,二者可以组合使用:主线程负责 UI,Web Worker 负责计算,Service Worker 负责网络和缓存,IndexedDB 负责持久化数据。这样可以构建出更流畅、更可靠、更接近原生体验的现代 Web 应用。