Appearance
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'实际影响与建议
实际影响
- Worker 内存独立计算:Worker 分配的内存从渲染进程的 V8 堆中分配
- 不直接占用主线程内存:但会增加渲染进程的总内存使用
- postMessage 大对象:大对象被拷贝,显著增加内存占用
- 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 面板
- 拍 Heap Snapshot
- 在 "JavaScript VM instances" 筛选 Worker
- 对比主线程和 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 保证原子性 |