Web Workers多线程

张玥 2026年9月17日 阅读时间 45分钟
JavaScript Web Workers 性能优化 多线程 并发编程
JavaScript编程性能优化之Web Workers多线程

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 动画完全卡死
  // - 输入框无法输入任何字符
});

// 更糟的是:浏览器会先执行同步代码,
// 页面连"加载中"都来不及渲染出来。
主线程时间轴
渲染
事件
长任务(阻塞 5s)
0ms 1000ms 5000ms+
红色区间内,用户输入全部被"卡住",交互响应延迟(INP)飙升。
解决思路:把这段计算搬到另一个线程去。

常见的"长任务元凶"包括:超大数组的排序与去重、JSON 的深度解析、图片像素级处理、加密解密、CSV/Excel 解析、复杂数学运算(如加密、压缩、路径规划)以及语法高亮等。它们共同的特征是——纯计算、不依赖 DOM。这恰恰就是 Web Workers 最擅长处理的场景。

Web Workers 是什么:浏览器里的真并行

Web Worker 是 HTML5 引入的一项标准,它允许网页在后台线程中运行 JavaScript。Worker 线程与主线程并行执行,拥有独立的执行栈、独立的内存空间和独立的全局上下文(DedicatedWorkerGlobalScope),二者之间通过消息传递进行通信,不共享普通的 JS 对象。

/* 主线程与 Worker 的关系(概念模型) */

// 主线程(Main Thread)
// - 负责渲染、事件、DOM 操作
// - 通过 postMessage 发送任务
// - 通过 onmessage 接收结果

// Worker 线程(Worker Thread)
// - 没有任何 DOM / window 对象
// - 拥有 self、navigator、fetch 等能力
// - 适合做纯计算、数据处理、网络请求编排

/* 关键特性 */
const features = [
  '真正的并行执行(不阻塞 UI)',
  '独立的全局作用域',
  '基于消息的通信',
  '受同源策略限制',
  '无法直接操作 DOM'
];
主线程 Main Thread
DOM · 渲染 · 事件 · 用户交互
postMessage → ← onmessage
Worker 线程 Worker Thread
计算 · 解析 · 加密 · 图像处理

两条线程并行运行,互不阻塞

第一个 Worker:完整生命周期

创建一个 Worker 只需要一行代码:传入脚本文件的 URL,浏览器便会启动一个新的后台线程去加载并执行它。下面是一个完整可运行的最小示例。

/* ---------- main.js(主线程) ---------- */
// 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.js(Worker 线程) ---------- */
// 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);
Worker 生命周期
1 创建 — new Worker(url) 启动新线程
2 加载 — 异步加载并执行脚本文件
3 通信 — postMessage / onmessage 双向收发
4 终止 — terminate() 或 self.close()
注意:Worker 脚本必须与主页面同源,且不能直接通过 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
深拷贝
克隆算法
Worker 对象
引用地址 B
大对象克隆有成本:拷贝 100MB 数据可能耗时数百毫秒。
解决方案:使用 Transferable Objects 实现零拷贝。

值得一提的是消息的顺序性:同一个 Worker 与主线程之间的消息严格按发送顺序投递,不会出现乱序。但消息的处理是异步的,Worker 收到消息后会进入自己的事件循环,因此发送方在 postMessage 之后不应假设任务已经完成。

/* 带请求 ID 的通信协议(避免结果错乱) */
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)。

/* ---------- 拷贝 vs 转移 ---------- */

// ❌ 拷贝: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
/* ---------- 共享内存:SharedArrayBuffer ---------- */
// 主线程:创建共享内存
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); // 唤醒
结构化克隆
O(n) 拷贝
安全、简单,大对象慢
Transferable
O(1) 转移
极快,但原线程失去所有权
SharedArrayBuffer
真共享
零拷贝,需 Atomics 同步
MessageChannel
点对点
适合多 Worker 互联
安全提示:由于 Spectre 等侧信道攻击,SharedArrayBuffer 需要页面处于跨源隔离状态才能使用,即响应头需包含 Cross-Origin-Opener-Policy: same-origin 与 Cross-Origin-Embedder-Policy: require-corp。

能力边界:能用什么,不能用什么

Worker 拥有独立的全局作用域,它并不是"另一个 window"。理解它的能力边界,能帮你快速判断一段代码是否适合放进 Worker。

/* ✅ 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
需要操作 DOM?让 Worker 返回数据,主线程负责渲染。

此外,Worker 还受到同源策略限制:只能加载与主页面同源的脚本。若需要通过 CDN 加载,可以先将脚本内容 fetch 回来,再用 Blob URL 创建 Worker。

Worker 池:把并发能力用到极致

频繁创建和销毁 Worker 是有成本的(线程初始化、脚本加载)。当需要处理大量小任务时,最佳实践是维护一个Worker 池:预先创建固定数量的 Worker,通过任务队列调度,复用线程资源。

/* worker-pool.js —— 轻量级 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 = [];
  }
}
池化调度模型
任务队列
T1 T2 T3 T4
↓ 调度器分配 ↓
Worker 1
执行中
Worker 2
执行中
Worker 3
空闲
Worker 4
空闲
池大小建议: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 });
};

// 整段像素运算完全在后台线程完成,
// 主线程的滚动、动画丝毫不受影响。
/* ---------- 案例二:大 CSV 文件解析 ---------- */
// 主线程:读取文件并转移 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 });
};
优化前后对比(解析 20MB CSV)
优化前(主线程)约 3200ms 卡顿
优化后(Worker)主线程 0ms 阻塞

计算总耗时几乎不变,但主线程始终保持可交互状态,用户体验天差地别。

模块化 Worker 与内联 Worker

现代构建工具让 Worker 的使用方式更加灵活。你可以用 ES Module 语法组织 Worker 代码,也可以把 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');
/* ---------- 方式二:内联 Worker(Blob URL) ---------- */
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);
独立文件 Worker
可缓存 / 可调试
推荐在生产环境使用
内联 Worker
零请求 / 灵活
适合小逻辑与库的封装
CSP 提示:内联 Worker 依赖 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 });
};
耗时拆解
序列化
传输
计算
回传
DevTools → Performance 面板录制
Sources 面板查看 Worker 线程
Lighthouse 检查 TBT / INP
若"序列化 + 传输"占比过高,就该考虑 Transferable 或改小数据粒度了。

Chrome DevTools 的 Performance 面板会把主线程和 Worker 线程分轨展示,你能直观看到主线程是否真的"解放"了。而在 Sources 面板中,Worker 脚本会以独立的线程条目出现,可以直接下断点调试,体验与普通脚本几乎一致。

Worker 家族:Dedicated / Shared / Service / Worklet

Web Worker 并非只有一种形态。理解它们各自的定位,才能选对工具。

/* 1. Dedicated Worker —— 专用 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');
// 运行在渲染 / 音频线程,用于高性能绘制与音频处理
Dedicated Worker
单页面专用 · 通用计算
Shared Worker
多页面共享 · 状态同步
Service Worker
离线缓存 · 请求代理
Worklet
渲染管线 · 音频处理
类型 作用域 典型用途 通信方式
Dedicated 单页面 CPU 密集计算 postMessage
Shared 多页面 跨页状态共享 MessagePort
Service 源级 离线 / 缓存 / 推送 postMessage
Worklet 渲染/音频线程 高性能绘制与音频 受限 API

最佳实践与常见陷阱

  • 不要为了用而用:只有耗时超过 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)) 这类写法
/* 推荐的 Worker 创建方式(兼容打包工具) */
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 处理微任务 → 通信延迟反而比直接执行更慢
一句话总结: 把 CPU 密集、与 DOM 无关、可被拆分或批处理的任务交给 Worker;把渲染、交互、状态管理留给主线程。二者各司其职,页面才能始终丝滑。