Web Workers
JavaScript 是单线程语言,主线程既要执行脚本又要处理渲染与交互,一旦出现耗时计算,页面就会卡住。Web Workers 提供了浏览器中的多线程能力,让重活、累活离开主线程。本文讲解 Worker 的创建与通信、能力边界、两种特殊 Worker(SharedWorker 与 Service Worker),以及数据传递与实战案例。
一、为什么需要 Worker
主线程阻塞问题
主线程同一时刻只能做一件事。一个百万级的循环、一次大数据量的解析,都会让界面"冻结":
// 在页面上执行这段代码,点击按钮会毫无反应直到循环结束
console.time("heavy");
let sum = 0;
for (let i = 0; i < 2_000_000_000; i++) {
sum += i;
}
console.timeEnd("heavy"); // 可能耗时数秒
// 期间页面无法点击、无法滚动、动画停摆Worker 的定位
Web Worker 让脚本在独立的线程中运行:主线程负责 UI 与交互,Worker 负责计算密集型任务,两者通过消息通信。但 Worker 线程无法访问 DOM,且受同源策略约束。
二、创建与通信
基本流程
// 主线程 main.js
const worker = new Worker("worker.js"); // 创建 Worker,指向脚本文件
worker.postMessage({ type: "compute", data: 1000000 }); // 发送任务
worker.onmessage = (event) => {
console.log("Worker 返回结果:", event.data);
};
worker.onerror = (err) => {
console.error("Worker 出错:", err.message);
};// worker.js —— 独立线程中运行
self.onmessage = (event) => {
const { type, data } = event.data;
if (type === "compute") {
let sum = 0;
for (let i = 0; i < data; i++) sum += i;
self.postMessage(sum); // 把结果发回主线程
}
};通信方向是双向的:主线程与 Worker 都可以 postMessage 给对方,也都能监听 onmessage。self 在 Worker 中指向 worker 自身的全局对象。
终止 Worker
worker.terminate(); // 立即终止,无法恢复,需要时重新 new WorkerWorker 内部也可以用 self.close() 自行关闭。不再使用的 Worker 应及时终止,避免资源泄漏。
常用参数
| 参数 | 说明 |
|---|---|
new Worker(url) | 传入脚本地址,需同源 |
{ type: "module" } | 以 ES Module 方式加载 Worker(支持 import) |
{ name: "myWorker" } | 给 Worker 命名,内部可用 self.name 读取 |
三、Worker 的能力与限制
| 分类 | 内容 |
|---|---|
| 不能使用 | document、window、parent、DOM 操作、localStorage/sessionStorage |
| 可以使用 | fetch、XMLHttpRequest、importScripts、setTimeout / setInterval、indexedDB、crypto、WebSocket、navigator 的部分属性 |
importScripts 引入其他脚本
Worker 脚本要复用工具函数时,用 importScripts 同步加载(模块 Worker 用 import):
// utils.js —— 被加载的公共工具
function formatNumber(n) {
return n.toLocaleString("zh-CN");
}
// worker.js
importScripts("utils.js"); // 同步加载并执行
self.onmessage = ({ data }) => {
self.postMessage(formatNumber(data)); // 直接使用 utils.js 中的函数
};错误处理
Worker 内抛出的未捕获异常会触发主线程的 worker.onerror:
worker.onerror = (event) => {
console.error("文件名:", event.filename);
console.error("行号:", event.lineno);
console.error("错误信息:", event.message);
};在 Worker 内部也可以自行捕获并回传错误对象,让主线程拿到结构化信息:
// worker.js
self.onmessage = ({ data }) => {
try {
riskyTask(data);
self.postMessage({ ok: true });
} catch (err) {
self.postMessage({ ok: false, error: err.message });
}
};四、SharedWorker:多页面共享
普通 Worker 每个页面各自创建、互不相通。SharedWorker 可以被同源的所有页面共享,多个标签页通过同一个 Worker 通信,适合做跨标签页状态同步(如在线用户数、消息中心)。
基本用法
// 主线程(每个页面)
const sharedWorker = new SharedWorker("shared-worker.js");
sharedWorker.port.postMessage({ type: "join", user: "张三" });
sharedWorker.port.onmessage = (event) => {
console.log("共享消息:", event.data);
};
sharedWorker.port.start(); // 显式启动端口// shared-worker.js
const ports = new Set(); // 记录所有连接的端口
self.onconnect = (event) => {
const port = event.ports[0]; // 每个新页面带来一个端口
ports.add(port);
port.onmessage = (e) => {
// 收到一个页面的消息,广播给所有页面
ports.forEach((p) => p.postMessage(`[${e.data.user}] 加入了`));
};
port.start();
};与普通 Worker 的对比
| 对比项 | 普通 Worker | SharedWorker |
|---|---|---|
| 共享范围 | 单页面独享 | 同源多页面共享 |
| 通信对象 | 直接 postMessage | 需通过 port 端口 |
| 生命周期 | 页面关闭即销毁 | 所有页面关闭后才销毁 |
| 浏览器兼容 | 全面支持 | 桌面端支持较好,移动端兼容性差 |
五、Service Worker 简介
Service Worker 是浏览器背后的网络代理:它独立于页面运行,可以拦截网络请求、读取缓存,是 PWA(渐进式 Web 应用)离线的核心技术。
生命周期
注册 → 安装 install → 激活 activate → 拦截请求 fetch// 注册(主线程)
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").then(() => {
console.log("Service Worker 注册成功");
});
}// sw.js
const CACHE_NAME = "my-app-v1";
const ASSETS = ["/", "/index.html", "/app.js", "/style.css"];
// 安装:预缓存关键资源
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
);
self.skipWaiting();
});
// 激活:清理旧缓存
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== CACHE_NAME)
.map((key) => caches.delete(key))
)
)
);
self.clients.claim();
});
// fetch:缓存优先,找不到再走网络(Cache First 策略)
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return (
cached ||
fetch(event.request).then((response) => {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
return response;
})
);
})
);
});向页面发消息
// 在 Service Worker 中主动通知页面
self.clients.matchAll().then((clients) => {
clients.forEach((client) =>
client.postMessage({ type: "update-available" })
);
});
// 页面监听
navigator.serviceWorker.addEventListener("message", (event) => {
console.log("收到 Service Worker 消息:", event.data);
});Service Worker 还包含消息推送(Push API)、后台同步(Background Sync)等能力,是构建"类原生"体验的基石。
六、数据传递:结构化克隆与可转移对象
postMessage 传递数据默认使用结构化克隆算法(Structured Clone):对象会被深拷贝到另一线程,支持数组、对象、Map、Set、Date、ArrayBuffer 等,但不支持函数、DOM 节点。
结构化克隆的特点
| 特性 | 说明 |
|---|---|
| 深拷贝 | 修改副本不影响原对象 |
| 支持类型 | 绝大多数可序列化类型 |
| 性能 | 数据量大时拷贝耗时明显 |
| 传输单位 | 每次消息传递一份完整副本 |
Transferable Objects(可转移对象)
对于 ArrayBuffer 等可转移对象,可以通过 postMessage 的第二个参数把所有权转移(Transfer)给接收方——不复制,直接移交内存,速度快且零拷贝:
// 主线程
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // 第二个参数指定转移列表
console.log(buffer.byteLength); // 0 —— 主线程的 buffer 已被"掏空"// worker.js —— 接收方
self.onmessage = ({ data }) => {
// data 是一个完整的 ArrayBuffer,独占所有权
const view = new Uint8Array(data);
view[0] = 255;
// 处理完可以再转移回主线程
self.postMessage(view.buffer, [view.buffer]);
};| 对比项 | 结构化克隆(默认) | 可转移对象(Transfer) |
|---|---|---|
| 是否复制 | 深拷贝一份 | 不复制,移交所有权 |
| 性能 | 大对象拷贝耗时 | 常数时间,零拷贝 |
| 原对象 | 保留,仍是完整数据 | 被转移,变成空壳 |
| 适用对象 | 任意可序列化数据 | 仅限 Transferable 类型(ArrayBuffer、MessagePort 等) |
七、实战案例
案例一:图片处理(像素反色)
把耗时的像素遍历移到 Worker,主线程只负责展示:
// main.js
const worker = new Worker("image-worker.js");
const canvas = document.getElementById("preview");
const ctx = canvas.getContext("2d");
function processImage() {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
worker.postMessage(imageData.data.buffer, [imageData.data.buffer]); // 转移 ArrayBuffer
}
worker.onmessage = ({ data }) => {
ctx.putImageData(
new ImageData(new Uint8ClampedArray(data), canvas.width, canvas.height),
0, 0
);
};// image-worker.js
self.onmessage = ({ data }) => {
const bytes = new Uint8ClampedArray(data);
for (let i = 0; i < bytes.length; i += 4) {
bytes[i] = 255 - bytes[i]; // R 反色
bytes[i + 1] = 255 - bytes[i + 1]; // G 反色
bytes[i + 2] = 255 - bytes[i + 2]; // B 反色
}
self.postMessage(bytes.buffer, [bytes.buffer]); // 再转移回去
};案例二:大数计算(判断质数)
// main.js
const worker = new Worker("prime-worker.js");
worker.postMessage(9999991);
worker.onmessage = ({ data }) => {
console.log(data.isPrime ? "是质数" : "不是质数");
};// prime-worker.js
function isPrime(n) {
if (n < 2) return false;
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false;
}
return true;
}
self.onmessage = ({ data }) => {
self.postMessage({ number: data, isPrime: isPrime(data) });
};案例三:数据分片处理
上万条记录需要逐条校验或转换时,把数组切成若干片,交给多个 Worker 并行处理:
// main.js
const records = generateLargeArray(); // 一万条记录
const workerCount = 4;
const chunkSize = Math.ceil(records.length / workerCount);
let done = 0;
const results = [];
for (let i = 0; i < workerCount; i++) {
const worker = new Worker("chunk-worker.js");
const chunk = records.slice(i * chunkSize, (i + 1) * chunkSize);
worker.postMessage(chunk);
worker.onmessage = ({ data }) => {
results.push(...data);
done++;
if (done === workerCount) {
console.log("全部处理完成,共", results.length, "条");
}
worker.terminate(); // 用后即焚
};
}// chunk-worker.js
self.onmessage = ({ data }) => {
const processed = data.map((item) => normalize(item));
self.postMessage(processed);
};使用建议:不要把过于琐碎的任务扔给 Worker——线程创建本身有开销,且数据拷贝(或转移)也有成本;只有主线程上确实耗时、阻塞交互的任务才值得移入 Worker。同时注意 Worker 数量有限(通常受浏览器限制),避免为每条小任务都新建线程。
掌握 Worker 的通信模型、数据转移技巧与三类 Worker 的职责划分,就能把页面中的重计算、后台任务与离线能力安排得井井有条。