Skip to content

Worker ​

进程与内存模型 ​

Worker 运行在哪个进程? ​

关键结论:Worker 运行在渲染进程内,但有独立的内存空间。

浏览器进程架构大致如下:

┌─────────────────────────────────────────────────────┐
│                    浏览器进程                        │
│  (地址空间、文件句柄、全局状态)                       │
├─────────────────────────────────────────────────────┤
│                   渲染进程                            │
│  ┌──────────────┐  ┌──────────────┐                │
│  │   主线程      │  │  Worker 线程  │                │
│  │  (V8 堆)     │  │  (独立 V8 堆) │                │
│  │              │  │              │                │
│  │  DOM         │  │  无 DOM       │                │
│  │  JS 全局对象  │  │  独立全局对象  │                │
│  └──────────────┘  └──────────────┘                │
│                                                      │
│  两个线程共享渲染进程地址空间,但不共享 JavaScript 对象 │
└─────────────────────────────────────────────────────┘

不同类型 Worker 的内存模型 ​

Worker 类型运行位置内存来源生命周期
DedicatedWorker渲染进程内渲染进程的 V8 堆(独立)与创建它的页面同生同灭
SharedWorker渲染进程内渲染进程的 V8 堆(共享)所有引用它的标签页共享
ServiceWorker独立进程独立进程的 V8 堆独立于页面,可后台运行
Worklet渲染进程内渲染进程的 V8 堆短暂存在,任务结束即释放

DedicatedWorker 内存示意图:

渲染进程 V8 堆内存
┌─────────────────────┬─────────────────────┐
│     主线程 V8 堆     │  DedicatedWorker V8 堆│
│                     │                     │
│  main.js 栈/堆      │   worker.js 栈/堆    │
│  ┌─────────────┐   │   ┌─────────────┐   │
│  │ globalVars  │   │   │ workerVars  │   │
│  │ {data: 100MB}│   │   │ {calc: ...} │   │
│  └─────────────┘   │   └─────────────┘   │
│                     │                     │
│  postMessage()  ────拷贝───────────────>│
│  (大对象复制,内存翻倍)                   │
└─────────────────────┴─────────────────────┘

ServiceWorker 的特殊之处 ​

ServiceWorker 运行在独立进程,不依附于任何渲染进程:

┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│   渲染进程 A    │  │   渲染进程 B    │  │   渲染进程 C    │
│   (标签页 A)    │  │   (标签页 B)    │  │   (标签页 C)    │
└────────┬────────┘  └────────┬────────┘  └────────┬────────┘
         │                      │                      │
         └──────────────────────┼──────────────────────┘
                                │
                    ┌───────────▼───────────┐
                    │   ServiceWorker      │
                    │   (独立进程)          │
                    │   独立 V8 堆          │
                    └─────────────────────┘

Worker 内存分配机制 ​

内存来源 ​

Worker 分配的内存从渲染进程的 V8 堆中分配,但使用独立的 V8 实例:

js
// main.js - 主线程
const worker = new Worker('worker.js');

// worker.js - Worker 线程
// 这里分配的内存来自渲染进程 V8 堆,但与主线程独立
const largeArray = new Array(10 * 1024 * 1024); // 约 80MB
console.log(largeArray.length); // 10000000

内存是否"叠加"? ​

会叠加,但不是简单的相加。

  • Worker 的内存是渲染进程 V8 总堆内存的一部分
  • 主线程和 Worker 各有独立的 V8 堆,互不重叠
  • 进程总内存 = 主线程堆 + Worker 堆 + 共享开销

可以用 Chrome DevTools 验证:

Memory 面板 → 左侧选择 "JavaScript VM 实例"
→ 可以看到主线程和 Worker 的独立堆

postMessage 与内存拷贝 ​

问题:postMessage 大对象的代价 ​

使用 postMessage 传递大对象时,数据会被结构化克隆算法(Structured Clone)拷贝:

js
// main.js
const bigData = new Array(10 * 1024 * 1024); // 80MB
worker.postMessage({ data: bigData }); // 触发 80MB 拷贝
console.log(bigData.length); // 主线程仍持有这份数据

// 内存变化:
// 拷贝前:主线程 80MB
// 拷贝后:主线程 80MB + Worker 80MB = 160MB

拷贝过程示意 ​

postMessage({ data: bigArray })
         │
         ▼ 结构化克隆算法
    ┌────────────┐
    │  序列化     │  遍历对象,构建新的内存副本
    │  bigArray  │  这是一个递归过程,大对象耗时显著
    └─────┬──────┘
          │ 生成独立副本
          ▼
    ┌────────────┐
    │ Worker 堆  │  Worker 获得完整副本
    │ bigArray   │
    └────────────┘

主线程和 Worker 各持有一份完整数据

Transferable 对象:零拷贝转移 ​

使用 Transferable 接口转移所有权,原对象被清空:

js
// main.js
const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
worker.postMessage(buffer, [buffer]); // 转移所有权

console.log(buffer.byteLength); // 0 - 所有权已转移,缓冲区被清空

// 内存变化:
// 转移前:主线程 100MB
// 转移后:主线程 0MB + Worker 100MB = 仍是 100MB(无拷贝开销)

常见 Transferable 类型:

类型说明
ArrayBuffer固定长度二进制缓冲区
MessagePort消息端口
ImageBitmap位图数据
OffscreenCanvas离屏画布
ReadableStream / WritableStream流对象

SharedArrayBuffer:共享内存 ​

SharedArrayBuffer 允许多个线程共享同一块内存,无需拷贝:

js
// main.js
const sharedBuffer = new SharedArrayBuffer(100 * 1024 * 1024);
const mainView = new Int32Array(sharedBuffer);
worker.postMessage({ sharedBuffer }, [sharedBuffer]);

// worker.js
self.onmessage = (e) => {
  const workerView = new Int32Array(e.data.sharedBuffer);
  workerView[0] = 42; // 主线程立即可见
};

⚠️ 注意:使用 SharedArrayBuffer 需要配合 SharedArrayBuffer 隔离策略和 crossOriginIsolation:

js
// 服务器响应头
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'

实际影响与建议 ​

实际影响 ​

  1. Worker 内存独立计算:Worker 分配的内存从渲染进程的 V8 堆中分配
  2. 不直接占用主线程内存:但会增加渲染进程的总内存使用
  3. postMessage 大对象:大对象被拷贝,显著增加内存占用
  4. Transferable vs SharedArrayBuffer:
    • Transferable:转移所有权,不拷贝,适合一次性大数据传递
    • SharedArrayBuffer:共享内存,无需拷贝,适合频繁读写场景

使用建议 ​

  • 计算密集型任务:使用 Worker,避免阻塞主线程
  • 大文件/大数据处理:使用 Worker + Transferable 对象
  • 频繁数据交换:使用 SharedArrayBuffer
  • 避免大对象 postMessage:每次传递都会完整拷贝,造成内存翻倍
  • 及时终止 Worker:用完后调用 worker.terminate(),释放内存
js
// 正确做法:大数据用 Transferable
const buffer = new ArrayBuffer(100 * 1024 * 1024);
worker.postMessage(buffer, [buffer]);

// 正确做法:不再使用时终止
worker.terminate();

调试 Worker 内存 ​

Chrome DevTools Memory 面板 ​

  1. 拍 Heap Snapshot
  2. 在 "JavaScript VM instances" 筛选 Worker
  3. 对比主线程和 Worker 的堆大小

performance.memory ​

js
// 仅 Chrome 可用
console.log(performance.memory);
// {
//   jsHeapSizeLimit: 2197815296,
//   totalJSHeapSize: 15000000,    // 当前 JS 堆总大小
//   usedJSHeapSize: 12000000      // 已使用的 JS 堆
// }

监控 Worker 内存 ​

js
// worker.js - 定期报告内存使用
setInterval(() => {
  if (performance.memory) {
    const { usedJSHeapSize, totalJSHeapSize } = performance.memory;
    self.postMessage({
      type: 'memory-report',
      used: usedJSHeapSize,
      total: totalJSHeapSize
    });
  }
}, 5000);

常见内存问题 ​

问题症状解决方案
postMessage 大对象拷贝内存翻倍、页面卡顿使用 Transferable 对象
Worker 未终止内存持续增长完成后调用 terminate()
Worker 闭包泄漏Worker 堆内存持续增长避免在 Worker 中保存大对象引用
SharedArrayBuffer 滥用多线程同时写入冲突使用 Atomics 保证原子性