JavaScript 从诞生之日起就被贴上了"单线程"的标签:所有代码都在主线程上排队执行,一个耗时循环就能让整个页面彻底冻结。但浏览器其实早就给了我们一把钥匙——Web Workers。它允许我们在后台线程中运行脚本,把 CPU 密集型计算从主线程中剥离出去,从而实现真正的并行执行。本文将从主线程阻塞的根因讲起,系统梳理 Web Workers 的 API、通信机制、数据传输优化、Worker 池、共享内存、实战案例、调试手段与最佳实践。45 分钟沉浸式学习,帮你彻底掌握浏览器端多线程编程。
主线程的困境:为什么会卡
浏览器中的 JavaScript 运行在单线程之上,这个线程被称为主线程(Main Thread)。它不仅仅负责执行 JS,还要承担解析 HTML、计算样式、布局、绘制、合成以及响应用户输入等几乎所有工作。也就是说,只要主线程被一段长任务占用,页面上的所有交互都会被迫排队等待。
document.querySelector('#btn').addEventListener('click', () => {
// 同步执行约 5 秒的重计算
let sum = 0;
for (let i = 0; i < 5_000_000_000; i++) {
sum += Math.sqrt(i);
}
console.log(sum);
// 在这 5 秒内:
// - 页面无法滚动
// - 按钮无法再次点击
// - CSS 动画完全卡死
// - 输入框无法输入任何字符
});
// 更糟的是:浏览器会先执行同步代码,
// 页面连"加载中"都来不及渲染出来。
常见的"长任务元凶"包括:超大数组的排序与去重、JSON 的深度解析、图片像素级处理、加密解密、CSV/Excel 解析、复杂数学运算(如加密、压缩、路径规划)以及语法高亮等。它们共同的特征是——纯计算、不依赖 DOM。这恰恰就是 Web Workers 最擅长处理的场景。
Web Workers 是什么:浏览器里的真并行
Web Worker 是 HTML5 引入的一项标准,它允许网页在后台线程中运行 JavaScript。Worker 线程与主线程并行执行,拥有独立的执行栈、独立的内存空间和独立的全局上下文(DedicatedWorkerGlobalScope),二者之间通过消息传递进行通信,不共享普通的 JS 对象。
// 主线程(Main Thread)
// - 负责渲染、事件、DOM 操作
// - 通过 postMessage 发送任务
// - 通过 onmessage 接收结果
// Worker 线程(Worker Thread)
// - 没有任何 DOM / window 对象
// - 拥有 self、navigator、fetch 等能力
// - 适合做纯计算、数据处理、网络请求编排
/* 关键特性 */
const features = [
'真正的并行执行(不阻塞 UI)',
'独立的全局作用域',
'基于消息的通信',
'受同源策略限制',
'无法直接操作 DOM'
];
两条线程并行运行,互不阻塞
第一个 Worker:完整生命周期
创建一个 Worker 只需要一行代码:传入脚本文件的 URL,浏览器便会启动一个新的后台线程去加载并执行它。下面是一个完整可运行的最小示例。
// 1. 创建 Worker,传入脚本路径
const worker = new Worker('./worker.js');
// 2. 发送任务给 Worker
worker.postMessage({
type: 'sum',
payload: [1, 2, 3, 4, 5]
});
// 3. 监听 Worker 返回的结果
worker.onmessage = (event) => {
console.log('收到结果:', event.data);
};
// 4. 监听错误
worker.onerror = (event) => {
console.error('Worker 出错:', event.message);
};
// 5. 任务完成后终止 Worker(释放资源)
// worker.terminate();
// Worker 中 self 指向 WorkerGlobalScope
self.onmessage = (event) => {
const { type, payload } = event.data;
if (type === 'sum') {
const result = payload.reduce((a, b) => a + b, 0);
// 把结果回传给主线程
self.postMessage({ type: 'result', result });
}
};
// 也可以使用 addEventListener 写法
// self.addEventListener('message', handler);
new Worker(url) 启动新线程
postMessage / onmessage 双向收发
terminate() 或 self.close()
file:// 协议加载。
消息通信:postMessage 与结构化克隆
主线程与 Worker 之间不能共享普通变量,一切数据都必须通过 postMessage 传递。浏览器会使用结构化克隆算法(Structured Clone Algorithm)对数据进行深拷贝,因此传递的是副本而非引用。
worker.postMessage({
// 基本类型
num: 42,
str: 'hello',
bool: true,
nil: null,
// 复杂结构
arr: [1, 2, { nested: true }],
obj: { a: 1, b: { c: 2 } },
date: new Date(),
regex: /abc/gi,
map: new Map([['k', 'v']]),
set: new Set([1, 2]),
buf: new ArrayBuffer(16)
});
/* 不支持传递的内容(会抛出 DataCloneError) */
// - 函数 / 闭包
// - DOM 节点
// - 原型链上的自定义属性
// - Symbol、WeakMap、WeakSet
引用地址 A
克隆算法
引用地址 B
值得一提的是消息的顺序性:同一个 Worker 与主线程之间的消息严格按发送顺序投递,不会出现乱序。但消息的处理是异步的,Worker 收到消息后会进入自己的事件循环,因此发送方在 postMessage 之后不应假设任务已经完成。
let seq = 0;
const pending = new Map();
function callWorker(type, payload) {
return new Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
worker.postMessage({ id, type, payload });
});
}
worker.onmessage = (event) => {
const { id, result, error } = event.data;
const task = pending.get(id);
if (!task) return;
pending.delete(id);
error ? task.reject(new Error(error)) : task.resolve(result);
};
// 使用方式:像调用异步函数一样自然
// const result = await callWorker('sum', [1, 2, 3]);
零拷贝:Transferable Objects 与共享内存
结构化克隆虽然安全,但面对大体积数据时开销巨大。为此,标准提供了 Transferable Objects(可转移对象):将对象的所有权直接"移交"给接收方,而不是复制。移交之后,原线程中的该对象会被置为不可用(byteLength 变为 0)。
// ❌ 拷贝:100MB 数据被完整复制一份
const buffer = new ArrayBuffer(100 * 1024 * 1024);
worker.postMessage(buffer);
console.log(buffer.byteLength); // 104857600(仍然可用)
// ✅ 转移:所有权移交,零拷贝
const buffer2 = new ArrayBuffer(100 * 1024 * 1024);
worker.postMessage(buffer2, [buffer2]);
console.log(buffer2.byteLength); // 0(已被转移)
/* 可转移的对象类型 */
// ArrayBuffer、MessagePort、
// ImageBitmap、OffscreenCanvas、ReadableStream
// 主线程:创建共享内存
const sab = new SharedArrayBuffer(1024);
const view = new Int32Array(sab);
worker.postMessage(sab); // 无需转移列表
// Worker:直接读写同一块内存
self.onmessage = (e) => {
const arr = new Int32Array(e.data);
arr[0] = 100; // 主线程立即可见
};
/* 使用 Atomics 保证并发安全 */
Atomics.add(view, 0, 1);
Atomics.load(view, 0);
Atomics.store(view, 0, 42);
Atomics.wait(view, 0, 0); // 阻塞等待
Atomics.notify(view, 0, 1); // 唤醒
SharedArrayBuffer 需要页面处于跨源隔离状态才能使用,即响应头需包含 Cross-Origin-Opener-Policy: same-origin 与 Cross-Origin-Embedder-Policy: require-corp。
能力边界:能用什么,不能用什么
Worker 拥有独立的全局作用域,它并不是"另一个 window"。理解它的能力边界,能帮你快速判断一段代码是否适合放进 Worker。
self // Worker 全局对象
fetch() // 网络请求
XMLHttpRequest // 传统 AJAX
WebSocket // 长连接
IndexedDB // 本地数据库
Cache // 缓存 API
setTimeout / setInterval
crypto.subtle // 加密
TextEncoder / TextDecoder
OffscreenCanvas // 离屏画布
importScripts() // 加载脚本
/* ❌ Worker 中不可用的能力 */
window // 不存在
document // 无法操作 DOM
alert() / confirm()
localStorage // 不可访问
parent / top
postMessage 之外的 UI 接口
fetch
IndexedDB
crypto
OffscreenCanvas
document
localStorage
此外,Worker 还受到同源策略限制:只能加载与主页面同源的脚本。若需要通过 CDN 加载,可以先将脚本内容 fetch 回来,再用 Blob URL 创建 Worker。
Worker 池:把并发能力用到极致
频繁创建和销毁 Worker 是有成本的(线程初始化、脚本加载)。当需要处理大量小任务时,最佳实践是维护一个Worker 池:预先创建固定数量的 Worker,通过任务队列调度,复用线程资源。
class WorkerPool {
constructor(url, size = navigator.hardwareConcurrency || 4) {
this.size = size;
this.url = url;
this.workers = [];
this.idle = [];
this.queue = [];
this.seq = 0;
this.callbacks = new Map();
this._init();
}
_init() {
for (let i = 0; i < this.size; i++) {
const w = new Worker(this.url);
w.onmessage = (e) => this._onMessage(w, e);
this.workers.push(w);
this.idle.push(w);
}
}
exec(type, payload, transfer = []) {
return new Promise((resolve, reject) => {
const id = ++this.seq;
this.callbacks.set(id, { resolve, reject });
this.queue.push({ id, type, payload, transfer });
this._dispatch();
});
}
_dispatch() {
while (this.queue.length && this.idle.length) {
const worker = this.idle.pop();
const task = this.queue.shift();
worker.busyId = task.id;
worker.postMessage({ id: task.id, type: task.type, payload: task.payload }, task.transfer);
}
}
_onMessage(worker, event) {
const { id, result, error } = event.data;
const cb = this.callbacks.get(id);
if (cb) {
this.callbacks.delete(id);
error ? cb.reject(new Error(error)) : cb.resolve(result);
}
worker.busyId = null;
this.idle.push(worker);
this._dispatch();
}
destroy() {
this.workers.forEach(w => w.terminate());
this.workers = [];
this.idle = [];
}
}
执行中
执行中
空闲
空闲
navigator.hardwareConcurrency
需要注意的是,池的大小并非越大越好。Worker 数量超过 CPU 逻辑核心数后,线程切换反而会带来额外开销。一般来说,池大小取 navigator.hardwareConcurrency 或略小于它的值(如 核心数 - 1),为主线程保留一个核心是比较稳妥的策略。
实战案例:图像处理与大数据解析
下面通过两个真实场景,看看 Web Workers 如何显著改善用户体验。
// 主线程:把 ImageBitmap 转移给 Worker
const canvas = document.querySelector('#canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('./image-worker.js');
worker.postMessage(
{ type: 'grayscale', canvas: offscreen },
[offscreen] // 转移所有权
);
/* image-worker.js */
self.onmessage = ({ data }) => {
const ctx = data.canvas.getContext('2d');
const img = ctx.getImageData(0, 0, 800, 600);
const d = img.data;
for (let i = 0; i < d.length; i += 4) {
const gray = d[i] * 0.299 + d[i+1] * 0.587 + d[i+2] * 0.114;
d[i] = d[i+1] = d[i+2] = gray;
}
ctx.putImageData(img, 0, 0);
self.postMessage({ done: true });
};
// 整段像素运算完全在后台线程完成,
// 主线程的滚动、动画丝毫不受影响。
// 主线程:读取文件并转移 ArrayBuffer
async function parseCSV(file) {
const buffer = await file.arrayBuffer();
return new Promise((resolve) => {
const w = new Worker('./csv-worker.js');
w.onmessage = (e) => { resolve(e.data); w.terminate(); };
w.postMessage(buffer, [buffer]); // 零拷贝
});
}
/* csv-worker.js */
self.onmessage = (e) => {
const text = new TextDecoder('utf-8').decode(e.data);
const rows = text.split('\n').map(line => line.split(','));
const header = rows.shift();
const records = rows.map(row =>
Object.fromEntries(header.map((key, i) => [key, row[i]]))
);
self.postMessage({ header, records, count: records.length });
};
计算总耗时几乎不变,但主线程始终保持可交互状态,用户体验天差地别。
模块化 Worker 与内联 Worker
现代构建工具让 Worker 的使用方式更加灵活。你可以用 ES Module 语法组织 Worker 代码,也可以把 Worker 直接内联在页面中,避免额外的网络请求。
const worker = new Worker('./worker.js', {
type: 'module' // 启用 ES Module
});
/* worker.js 中可以直接 import */
import { heavyCompute } from './utils/compute.js';
import { formatResult } from './utils/format.js';
self.onmessage = (e) => {
self.postMessage(formatResult(heavyCompute(e.data)));
};
/* 传统写法:importScripts */
importScripts('./lib/underscore.js', './lib/math.js');
function createInlineWorker(fn) {
const code = `
self.onmessage = function (e) {
const result = (${fn.toString()})(e.data);
self.postMessage(result);
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
return new Worker(url);
}
// 使用示例
const w = createInlineWorker((data) => {
return data.map(n => n * n).reduce((a, b) => a + b, 0);
});
// 注意:用完记得释放 Blob URL
// URL.revokeObjectURL(url);
Blob URL,如果页面设置了严格的 Content-Security-Policy,需要允许 worker-src blob:,否则会被拦截。
性能测量与调试技巧
把任务搬进 Worker 并不等于万事大吉。你需要能量化地知道:通信开销有多大?任务到底快了多少?瓶颈在哪里?
performance.mark('task-start');
worker.postMessage({ type: 'heavy', payload });
worker.onmessage = () => {
performance.mark('task-end');
performance.measure('worker-task', 'task-start', 'task-end');
const [m] = performance.getEntriesByName('worker-task');
console.log(`总耗时 ${m.duration.toFixed(2)}ms`);
};
/* Worker 内部也可以精确计时 */
self.onmessage = (e) => {
const t0 = performance.now();
const result = doHeavyWork(e.data);
const cost = performance.now() - t0;
self.postMessage({ result, cost });
};
Chrome DevTools 的 Performance 面板会把主线程和 Worker 线程分轨展示,你能直观看到主线程是否真的"解放"了。而在 Sources 面板中,Worker 脚本会以独立的线程条目出现,可以直接下断点调试,体验与普通脚本几乎一致。
Worker 家族:Dedicated / Shared / Service / Worklet
Web Worker 并非只有一种形态。理解它们各自的定位,才能选对工具。
const w = new Worker('./worker.js');
// 仅当前页面可用,最常见的形态
/* 2. Shared Worker —— 共享 Worker */
const sw = new SharedWorker('./shared.js');
sw.port.start();
sw.port.postMessage('hello');
// 可被多个页面 / iframe 共享同一线程
/* 3. Service Worker —— 网络代理 */
navigator.serviceWorker.register('/sw.js');
// 用于离线缓存、请求拦截、消息推送
/* 4. Worklet —— 渲染管线级扩展 */
CSS.paintWorklet.addModule('paint.js');
audioWorklet.addModule('audio.js');
// 运行在渲染 / 音频线程,用于高性能绘制与音频处理
最佳实践与常见陷阱
- 不要为了用而用:只有耗时超过 50ms 的纯计算任务才值得搬进 Worker,小任务通信开销可能超过收益
- 大对象用 Transferable:ArrayBuffer、ImageBitmap、OffscreenCanvas 优先走转移而非克隆
- 复用而非频繁创建:使用 Worker 池,避免反复
new Worker()与terminate() - 设计清晰的消息协议:带
id、type、payload的结构化消息,便于扩展与调试 - 做好错误与超时处理:监听
onerror与onmessageerror,为长任务设计超时兜底 - 及时释放资源:页面卸载或组件销毁时调用
terminate(),避免线程泄漏 - 注意打包工具配置:Vite / Webpack 都支持
new Worker(new URL('./w.js', import.meta.url))这类写法
const worker = new Worker(
new URL('./workers/heavy.worker.js', import.meta.url),
{ type: 'module' }
);
/* 完整的错误处理模板 */
worker.onerror = (e) => {
console.error('Worker 运行时错误:', e.message, e.filename, e.lineno);
e.preventDefault(); // 阻止错误继续冒泡
};
worker.onmessageerror = (e) => {
console.error('消息反序列化失败:', e.data);
};
/* 组件卸载时清理 */
window.addEventListener('beforeunload', () => {
worker.terminate();
});
/* Worker 内部主动关闭 */
// self.close();
- 在 Worker 中访问
document或window→ 直接报错 - 传递了函数或 DOM 节点 →
DataCloneError - 转移了 ArrayBuffer 后继续在原线程使用 → 长度为 0,逻辑出错
- Worker 脚本路径错误 → 静默失败,只在
onerror中可见 - 忘记 terminate → 页面关闭后线程仍可能驻留
- 用 Worker 处理微任务 → 通信延迟反而比直接执行更慢