服务端2026年5月27日 12:46
Web Worker 7 大限制及解决方案一览## 为什么 Worker 有这么多限制
Worker 的限制不是偷懒,是设计上的安全选择。浏览器最核心的约束是:**DOM 操作不是线程安全的**。两个线程同时改同一个 DOM 节点,后果不可预测。所以 Worker 干脆被隔离了——不能碰 DOM、不能碰大部分浏览器 API,只能通过 postMessage 通信。
理解了这个前提,限制就不是"不能做什么",而是"怎么绕过去"。
## 限制一:不能访问 DOM
这是最大的限制。Worker 里没有 `document`、没有 `window`、没有任何 DOM API。
```javascript
// ❌ Worker 里直接报错
document.getElementById('app');
window.innerWidth;
```
**解决方式**:计算在 Worker 里做,DOM 操作回主线程执行。
```javascript
// Worker:只算数据
self.onmessage = (e) => {
const positions = calculateLayout(e.data.items);
self.postMessage({ positions });
};
// 主线程:拿到结果后操作 DOM
worker.onmessage = (e) => {
const { positions } = e.data;
positions.forEach(({ id, x, y }) => {
document.getElementById(id).style.transform = `translate(${x}px, ${y}px)`;
});
};
```
这个模式有个名字叫"数据驱动渲染"——Worker 产出数据,主线程负责映射到 DOM。虚拟 DOM 框架(React/Vue)天然适合这种模式:Worker 里做 diff 计算,把最小更新集传给主线程 apply。
如果需要频繁操作 Canvas,用 `OffscreenCanvas` 把 Canvas 上下文转移给 Worker:
```javascript
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
// Worker 里直接绘制
self.onmessage = (e) => {
const ctx = e.data.canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
};
```
## 限制二:不能用 localStorage
localStorage 是同步 API,多线程同时读写会产生竞态条件,所以 Worker 被禁止访问。
**解决方式**:用 IndexedDB 替代。IndexedDB 是异步的,Worker 可以直接使用。
```javascript
// Worker 里直接操作 IndexedDB
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (e) => {
e.target.result.createObjectStore('data', { keyPath: 'id' });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('data', 'readwrite');
tx.objectStore('data').put({ id: 1, value: 'from worker' });
};
```
如果你非要从 Worker 里读写 localStorage 的数据,让主线程做中转:
```javascript
// Worker 请求读取
self.postMessage({ type: 'getLocalStorage', key: 'token' });
// 主线程中转
worker.onmessage = (e) => {
if (e.data.type === 'getLocalStorage') {
const value = localStorage.getItem(e.data.key);
worker.postMessage({ type: 'localStorageResult', key: e.data.key, value });
}
};
```
但这样每读一次都要跨线程通信,性能很差。能用 IndexedDB 就用 IndexedDB。
## 限制三:不能发起 XHR 请求
XMLHttpRequest 的同步模式(`open(method, url, false)`)会阻塞线程,在 Worker 里被禁止。但异步 XHR 其实也不推荐——用 fetch 替代。
**解决方式**:Worker 里用 fetch,它是异步的且完全支持。
```javascript
// Worker 里直接发请求
self.onmessage = async (e) => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
self.postMessage({ data });
};
```
WebSocket 和 EventSource 也能在 Worker 里正常使用,不受限制。
## 限制四:不能加载跨域脚本
Worker 脚本必须和主页面同源。跨域 URL 直接创建会报 `SecurityError`。
**解决方式 1**:Blob URL 内联。
```javascript
// 先 fetch 跨域脚本内容,再创建 Blob Worker
const response = await fetch('https://cdn.example.com/worker.js');
const code = await response.text();
const blob = new Blob([code], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
```
注意:这绕过了同源限制但引入了新风险——你加载的跨域代码可能被篡改。确保 CDN 可信,最好配上 SRI(Subresource Integrity)。
**解决方式 2**:`importScripts` 可以加载跨域脚本(Worker 内部)。
```javascript
// worker.js
importScripts('https://cdn.example.com/lib.js');
```
`importScripts` 不受同源限制,但受 CSP 的 `script-src` 约束。
## 限制五:没有 window 对象
Worker 的全局对象是 `self`(`DedicatedWorkerGlobalScope`),不是 `window`。很多挂在 `window` 上的东西在 Worker 里不存在。
| 主线程有 | Worker 里 | 替代方案 |
|----------|-----------|----------|
| `window` | `self` | 直接用 `self` |
| `window.location` | `self.location`(只读) | 能读不能改 |
| `window.navigator` | `self.navigator` | 大部分属性可用 |
| `window.alert()` | 不存在 | 用 postMessage 通知主线程 |
| `window.setTimeout` | `self.setTimeout` | 正常可用 |
| `window.fetch` | `self.fetch` | 正常可用 |
| `window.indexedDB` | `self.indexedDB` | 正常可用 |
## 限制六:通信有序列化开销
postMessage 默认用结构化克隆,数据要拷贝一份。小数据无所谓,大数据(几 MB 以上)拷贝开销可能比计算本身还大。
**解决方式**:
| 方案 | 适用场景 | 原理 |
|------|----------|------|
| Transferable | 大 ArrayBuffer/Blob 单向传输 | 所有权转移,零拷贝 |
| SharedArrayBuffer | 高频双向读写同一块数据 | 共享内存,Atomics 同步 |
| 批量发送 | 大量小消息 | 攒批发,减少序列化次数 |
详见 [Web Worker 通信全解析](/topic/446298779224)。
## 限制七:脚本路径是相对 HTML 的
```javascript
// 如果 HTML 在 /pages/index.html
// Worker 脚本在 /workers/task.js
new Worker('task.js'); // ❌ 会找 /pages/task.js
new Worker('/workers/task.js'); // ✅ 绝对路径
```
在打包工具里更容易搞错。Vite/Webpack 5 的正确写法:
```javascript
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' }
);
```
`import.meta.url` 是当前模块的 URL,`new URL` 相对于它解析,打包工具会正确处理路径。
## 总结:一张表搞定
| 限制 | 解决方案 |
|------|----------|
| 不能访问 DOM | Worker 算数据,主线程操作 DOM;用 OffscreenCanvas |
| 不能用 localStorage | 用 IndexedDB 替代 |
| 不能用同步 XHR | 用 fetch 替代 |
| 不能加载跨域脚本 | Blob URL 或 importScripts |
| 没有 window 对象 | 用 self 替代 |
| 通信有序列化开销 | Transferable / SharedArrayBuffer / 批量发送 |
| 脚本路径问题 | `new URL('./worker.js', import.meta.url)` |
这些限制的本质就是一条:**Worker 是数据处理器,不是 UI 控制器**。把计算放进去,把渲染留在外面,架构对了限制就不是问题。
标签
Web Worker
Web Worker 是 HTML5 提供的一种在后台线程中运行脚本的机制。它允许网页脚本在后台线程中运行,而不会阻塞主线程,从而提高网页的性能和响应能力。例如,当一个网页需要进行大量的计算(如复杂的数据加密、图像渲染中的复杂算法等),如果在主线程中进行,会导致页面冻结,用户无法进行其他操作,如滚动页面、点击按钮等。而使用 Web Worker,这些计算任务可以放在后台线程中执行,主线程依然可以响应用户的交互操作。

服务端2026年5月27日 12:45
Web Worker 调试指南:DevTools、消息追踪与内存分析## Worker 调试为什么难
Worker 跑在独立线程里,`console.log` 能用但输出混在主线程日志里不好找,断点默认不生效,报错了堆栈和主线程是断开的。但只要知道工具在哪,调试 Worker 并不比调主线程难多少。
## Chrome DevTools:最常用的方式
### 找到 Worker 线程
打开 DevTools → Sources 面板 → 左侧 Threads 区域。主线程和 Worker 线程会分开列出,点击 Worker 线程就能看到它的源码、设断点、看调用栈。
如果 Threads 区域没出现 Worker,检查两个地方:
1. DevTools 设置(F1)→ 勾选"Workers"下的"Auto-expand"
2. 确认 Worker 已经被创建——在 Console 里输入 `chrome && chrome.debugger` 确认
### 在 Worker 里打断点
和主线程一样:Sources 面板里打开 Worker 的 JS 文件,点行号设断点。Worker 里代码执行到断点会暂停,主线程不受影响(但 postMessage 会排队等 Worker 恢复)。
### 专用 Worker 的 Console
Worker 里的 `console.log` 会输出到 DevTools Console,但前面没有线程标识,容易和主线程日志混淆。建议在 Worker 里加前缀:
```javascript
// worker.js
function log(...args) {
console.log('[Worker]', ...args);
}
log('开始处理数据', data.length);
```
### Shared Worker 和 Service Worker 的调试入口
这两种 Worker 不在页面的 DevTools 里直接显示,需要单独打开:
- **Shared Worker**:访问 `chrome://inspect/#workers`,能看到所有 Shared Worker 实例,点击 inspect 打开独立 DevTools 窗口
- **Service Worker**:DevTools → Application 面板 → Service Workers 区域,可以查看注册状态、手动触发 update、模拟推送事件
## console 之外的调试手段
### 结构化日志
比加前缀更进一步,用结构化日志让 Worker 的输出可追溯:
```javascript
// worker.js
function log(level, event, data = {}) {
console.log(JSON.stringify({
source: 'worker',
level,
event,
timestamp: Date.now(),
...data
}));
}
log('info', 'task-start', { taskId: 1, dataSize: 10000 });
log('error', 'task-failed', { taskId: 1, error: err.message });
```
这样日志可以统一采集和分析,线上排查问题时不用对着混在一起的 Console 猜哪条是 Worker 输出的。
### 消息日志:窥探通信内容
Worker 的 bug 经常出在通信环节——发了消息但格式不对,或者该回消息的没回。写一个消息拦截器记录所有 postMessage:
```javascript
// 主线程:拦截 Worker 通信
function createDebugWorker(url) {
const worker = new Worker(url);
const originalPostMessage = worker.postMessage.bind(worker);
worker.postMessage = (data, transfer) => {
console.log('[Main → Worker]', JSON.stringify(data).slice(0, 200));
originalPostMessage(data, transfer);
};
worker.onmessage = (e) => {
console.log('[Worker → Main]', JSON.stringify(e.data).slice(0, 200));
};
return worker;
}
const worker = createDebugWorker('worker.js');
```
Worker 端也加一层:
```javascript
// worker.js
const originalPostMessage = self.postMessage.bind(self);
self.postMessage = (data, transfer) => {
console.log('[Worker → Main]', JSON.stringify(data).slice(0, 200));
originalPostMessage(data, transfer);
};
```
这样每次通信都有日志,消息丢了、格式错了一目了然。上线前记得删掉或用环境变量控制开关。
### Performance 面板分析 Worker 性能
DevTools Performance 面板会录制所有线程的活动。录制一段操作后,在时间轴上能看到:
- Main 线程的活动(紫色是渲染,黄色是脚本)
- Worker 线程的活动(独立一行,黄色标记脚本执行)
- postMessage 的发送和接收时间点
如果发现 Worker 任务执行时间过长,点击对应的黄色条块能看到函数调用栈和耗时分布,精确定位热点函数。
## 常见调试场景
### Worker 没有响应
排查步骤:
1. 确认 Worker 创建成功——`worker.onerror` 有没有触发
2. 确认消息发出去了——用消息拦截器看 `[Main → Worker]` 日志
3. 确认 Worker 收到了消息——在 Worker 入口加 `log('received', e.data)`
4. 确认 Worker 没有卡在死循环——Performance 面板看 Worker 线程是否一直在执行
5. 确认 Worker 没有报错——检查 Console 是否有未捕获异常
最常见的两个原因:Worker 脚本路径错了(创建时就失败了,但 onerror 没监听),或者消息格式不匹配(Worker 里 `e.data.type` 判断分支没命中)。
### 内存泄漏
Worker 长时间运行后内存持续上涨:
1. DevTools → Memory 面板 → 选择 Worker 线程 → 拍 Heap Snapshot
2. 对比两次 Snapshot,看哪些对象只增不减
3. 常见原因:闭包引用了大对象、事件监听器没移除、定时器没清除
```javascript
// Worker 里常见的泄漏模式
self.onmessage = (e) => {
const hugeData = e.data;
// 泄漏:闭包引用了 hugeData,永远不会被 GC
setInterval(() => {
console.log(hugeData.length); // hugeData 被闭包持有
}, 1000);
};
```
修复方式:用完即释放,或者定时器保存引用,不需要时 `clearInterval`。
### Shared Worker 连不上
SharedWorker 的调试入口在 `chrome://inspect/#workers`。常见问题:
- `port.start()` 忘了调用——消息收不到但不报错
- 连接 URL 必须完全一致(包括 query string)——两个页面用不同 URL 创建的 SharedWorker 是两个独立实例
- 同源策略——不同源的页面不能共享同一个 Worker
## 调试工具速查
| 工具 | 用途 | 入口 |
|------|------|------|
| DevTools Sources | 断点、单步、调用栈 | F12 → Sources → Threads |
| DevTools Console | Worker 日志 | F12 → Console |
| DevTools Performance | Worker 性能分析 | F12 → Performance |
| DevTools Memory | Worker 内存快照 | F12 → Memory → 选 Worker 线程 |
| chrome://inspect/#workers | Shared/Service Worker 调试 | 地址栏直接访问 |
| Application → Service Workers | Service Worker 状态管理 | F12 → Application |
## 上线前的调试清理
调试代码(日志拦截器、前缀 console、消息追踪)上线前必须清理或条件化。推荐用环境变量控制:
```javascript
const DEBUG = typeof self !== 'undefined' && self.location?.search?.includes('debug=1');
function log(...args) {
if (DEBUG) console.log('[Worker]', ...args);
}
```
这样开发时 URL 加 `?debug=1` 就能看到 Worker 日志,线上默认关闭不影响性能。
服务端2026年5月27日 12:43
Dedicated、Shared、Service Worker:三种 Web Worker 详解## 三种 Worker,三种用途
浏览器里能叫"Worker"的有三种,干的事完全不一样:
| 类型 | 一句话定位 | 和页面关系 | 典型用途 |
|------|-----------|-----------|----------|
| Dedicated Worker | 后台计算线程 | 一对一,页面关了它就销毁 | 排序、解析、图像处理 |
| Shared Worker | 多页面共享的后台线程 | 多对一,所有同源页面共享 | 跨标签页状态同步 |
| Service Worker | 网络代理 + 离线缓存 | 独立生命周期,页面关了还活着 | PWA、离线、请求拦截 |
别搞混——Dedicated Worker 是拿来干活的,Shared Worker 是拿来共享的,Service Worker 是拿来代理网络的。
## Dedicated Worker:用得最多的那个
绝大多数时候你说的"Web Worker"就是它。一个页面创建,只有这个页面能用,页面关了 Worker 也跟着销毁。
```javascript
// 创建
const worker = new Worker('worker.js');
// 双向通信
worker.postMessage({ type: 'start', data: payload });
worker.onmessage = (e) => console.log('结果:', e.data);
// 关闭
worker.terminate();
```
也可以用 Blob URL 创建内联 Worker,不用单独的 JS 文件:
```javascript
const code = `
self.onmessage = (e) => {
const result = heavyCalc(e.data);
self.postMessage(result);
};
`;
const worker = new Worker(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })));
```
Dedicated Worker 的生命周期很简单:创建 → 运行 → terminate 或页面关闭。没有什么"激活""等待"状态,不需要管理复杂状态机。
## Shared Worker:跨标签页的共享线程
多个同源标签页可以共用同一个 Shared Worker 实例。适合做跨页面状态同步——比如用户在标签页 A 加了购物车商品,标签页 B 实时看到数量更新。
```javascript
// 每个页面都这样创建,浏览器会复用同一个实例
const worker = new SharedWorker('shared-worker.js');
// 注意:SharedWorker 用 port 通信,不是直接 onmessage
worker.port.start();
worker.port.postMessage({ type: 'cart-update', item: 'iPhone 17' });
worker.port.onmessage = (e) => {
console.log('收到:', e.data);
};
```
Worker 端也不一样,用 `onconnect` 接收新连接:
```javascript
// shared-worker.js
const clients = [];
self.onconnect = (e) => {
const port = e.ports[0];
clients.push(port);
port.onmessage = (event) => {
// 广播给所有连接的页面
clients.forEach(client => {
client.postMessage(event.data);
});
};
};
```
**Shared Worker 的坑**:
- 调试困难——Chrome DevTools 里要单独打开 Shared Worker 的调试面板(`chrome://inspect/#workers`)
- 所有连接断开后 Worker 才会销毁,不是最后一个页面关了就立刻死
- `port.start()` 容易忘写,忘写了消息收不到但也不报错
## Service Worker:不是普通 Worker
Service Worker 是三种里最特殊的。它不是用来做计算的,而是浏览器的网络代理层:
- **拦截请求**:页面发出的 fetch 请求先经过 Service Worker,可以改写响应、返回缓存
- **离线支持**:把资源缓存下来,断网时也能访问
- **推送通知**:即使页面没打开,也能收到服务端推送
- **后台同步**:网络恢复时自动重试失败的请求
```javascript
// 注册
navigator.serviceWorker.register('/sw.js');
// sw.js
self.addEventListener('install', (event) => {
// 安装时预缓存资源
event.waitUntil(
caches.open('v1').then(cache => cache.addAll(['/index.html', '/app.js']))
);
});
self.addEventListener('fetch', (event) => {
// 拦截请求,先查缓存
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
});
```
Service Worker 的生命周期和其他两种完全不同:
```
安装(install) → 激活(activate) → 运行中
↑ ↓
等待(waiting) ← 更新发现
```
关键区别:Service Worker 在页面关闭后仍然存活,浏览器会在需要时唤醒它。这也是为什么它能处理推送通知和后台同步。
**Service Worker 不能做的事**:同步 XHR、访问 DOM、访问 localStorage。和 Dedicated Worker 一样受 API 限制,但更严格——连 `self.localStorage` 都没有,只能用 Cache API 和 IndexedDB。
## 怎么选
| 场景 | 选哪个 |
|------|--------|
| 页面内耗时计算(排序、解析) | Dedicated Worker |
| 多标签页共享状态 | Shared Worker |
| 离线缓存、请求拦截 | Service Worker |
| 推送通知 | Service Worker |
| 后台数据同步 | Service Worker |
| 图像/音视频处理 | Dedicated Worker |
一个常见错误:用 Shared Worker 做计算密集型任务。Shared Worker 的设计初衷是共享状态,不是共享算力。如果多个页面同时往一个 Shared Worker 发计算任务,它还是单线程处理,反而互相等待。
另一个常见错误:把 Service Worker 当普通 Worker 用。Service Worker 的生命周期管理复杂,它会在不可预期的时间被浏览器唤醒和终止。在它里面做长耗时计算是不靠谱的——可能算到一半就被杀了。
服务端2026年5月27日 12:42
Web Worker 性能优化:通信、并行与内存管理## 先搞清楚瓶颈在哪
Worker 性能优化不是玄学,瓶颈就三个地方:**创建开销**、**通信开销**、**计算开销**。先 Profiler 看哪个是瓶颈,再对症下药,别瞎优化。
## 创建开销:复用比重建快 100 倍
`new Worker()` 不是免费的。浏览器要分配线程、解析脚本、初始化上下文,一次创建大概 10-50ms。如果你每次任务都新建再 terminate,开销比任务本身还大。
### Worker 池
和数据库连接池一个道理——预先创建好,任务来了分配,做完了归还:
```javascript
class WorkerPool {
constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
this.workers = [];
this.queue = [];
for (let i = 0; i < size; i++) {
const worker = new Worker(workerUrl);
worker.busy = false;
worker.onmessage = (e) => {
const { resolve } = worker.task;
delete worker.task;
worker.busy = false;
this.processQueue();
resolve(e.data);
};
this.workers.push(worker);
}
}
exec(data) {
return new Promise((resolve) => {
const worker = this.workers.find(w => !w.busy);
if (worker) {
worker.busy = true;
worker.task = { resolve };
worker.postMessage(data);
} else {
this.queue.push({ data, resolve });
}
});
}
processQueue() {
if (this.queue.length === 0) return;
const worker = this.workers.find(w => !w.busy);
if (!worker) return;
const { data, resolve } = this.queue.shift();
worker.busy = true;
worker.task = { resolve };
worker.postMessage(data);
}
}
// 使用
const pool = new WorkerPool('worker.js', 4);
const result = await pool.exec({ type: 'sort', data: largeArray });
```
Worker 池适合任务频繁但单个任务不太大的场景。如果任务很少(比如页面生命周期内就跑一两次),直接 `new Worker()` 就行,别过度设计。
## 通信开销:序列化才是大头
Worker 通信的瓶颈不在网络,在序列化。`postMessage` 默认用结构化克隆,数据量大的时候拷贝耗时惊人。
### Transferable:零拷贝传大数据
```javascript
const buffer = new Float64Array(1_000_000);
// 慢:结构化克隆,拷贝 8MB 数据
worker.postMessage({ data: buffer });
// 快:转移所有权,零拷贝
worker.postMessage({ data: buffer }, [buffer.buffer]);
// 注意:转移后主线程不能再访问 buffer
```
实测数据:
| 数据大小 | 结构化克隆 | Transferable |
|----------|-----------|--------------|
| 100KB | ~0.5ms | ~0.05ms |
| 1MB | ~5ms | ~0.1ms |
| 10MB | ~50ms | ~0.2ms |
10MB 以上的数据,不用 Transferable 等于白用 Worker。
### SharedArrayBuffer:跳过序列化
Transferable 虽然零拷贝,但只能单向传——发过去主线程就没了。如果你需要双向频繁读写同一块数据,用 SharedArrayBuffer:
```javascript
const shared = new SharedArrayBuffer(1024 * 1024);
const view = new Float64Array(shared);
// 主线程和 Worker 共享同一块内存
worker.postMessage({ shared });
// Worker 里直接读写
self.onmessage = (e) => {
const view = new Float64Array(e.data.shared);
Atomics.store(view, 0, 42); // 原子写入
};
```
需要配合 `Atomics` 做原子操作,服务端还要配 COOP/COEP 头,门槛比 Transferable 高。但高频通信场景下收益巨大——完全没有序列化开销。
### 批量发送:减少通信次数
每秒 postMessage 100 次和 1 次发 100 条数据,后者快得多。序列化有固定开销(即使数据很小也要走一遍结构化克隆流程),减少次数比减少数据量更有效:
```javascript
// 慢:逐条发送
data.forEach(item => worker.postMessage(item));
// 快:攒批发送
worker.postMessage({ batch: data });
```
## 计算开销:用多 Worker 并行
单 Worker 的计算速度和主线程 JS 一样,只是不卡 UI。要真正加速,得把任务拆给多个 Worker 并行跑:
```javascript
function parallelSort(data, workerCount = 4) {
const chunkSize = Math.ceil(data.length / workerCount);
const chunks = [];
for (let i = 0; i < workerCount; i++) {
chunks.push(data.slice(i * chunkSize, (i + 1) * chunkSize));
}
return Promise.all(chunks.map((chunk, i) => {
return new Promise((resolve) => {
const worker = new Worker('sort-worker.js');
worker.onmessage = (e) => resolve(e.data);
worker.postMessage(chunk);
});
})).then(sortedChunks => {
// 合并已排序的分片
return mergeSortedArrays(sortedChunks);
});
}
```
实测 100 万元素数组排序:
| 方案 | 耗时 |
|------|------|
| 主线程单线程 | ~800ms(UI 卡死) |
| 单 Worker | ~800ms(UI 正常) |
| 4 Worker 并行 | ~250ms(UI 正常) |
Worker 数量不要超过 CPU 核心数,`navigator.hardwareConcurrency` 可以拿到。多了反而会因为线程调度开销变慢。
## 内存管理
Worker 占的内存不会自动释放,必须显式 `terminate()`。如果页面生命周期内不再需要某个 Worker,立刻关掉:
```javascript
// 任务完成后关闭
worker.onmessage = (e) => {
handleResult(e.data);
worker.terminate(); // 释放线程和内存
};
// 或者超时强制关闭
const timeout = setTimeout(() => worker.terminate(), 30000);
worker.onmessage = (e) => {
clearTimeout(timeout);
handleResult(e.data);
};
```
长时间运行的 Worker 要注意内存泄漏——Worker 里的闭包、事件监听器、定时器如果不用了不清理,内存会持续上涨。在 Worker 里加个定期自检:
```javascript
setInterval(() => {
const used = performance.memory?.usedJSHeapSize;
if (used && used > 50 * 1024 * 1024) { // 超过 50MB
self.postMessage({ type: 'memory-warning', used });
}
}, 10000);
```
## 懒加载:按需创建 Worker
不是所有 Worker 都要在页面加载时就创建。用 `new URL()` + 动态 import 实现按需加载,首屏不需要的 Worker 等用到时再创建:
```javascript
async function getWorker() {
if (!workerInstance) {
workerInstance = new Worker(
new URL('./heavy-worker.js', import.meta.url),
{ type: 'module' }
);
}
return workerInstance;
}
// 用户点击"导出"按钮时才创建
button.onclick = async () => {
const worker = await getWorker();
worker.postMessage(exportData);
};
```
## 优化优先级
按收益从大到小排:
1. **Transferable 替代结构化克隆**(大数据场景立竿见影)
2. **Worker 池复用**(频繁创建销毁场景收益大)
3. **批量发送减少通信次数**(高频小消息场景)
4. **多 Worker 并行**(计算密集型场景)
5. **SharedArrayBuffer**(超高频双向通信场景,门槛高但收益最大)
6. **懒加载**(首屏性能敏感场景)
服务端2026年5月27日 12:40
Web Worker 安全攻防:同源策略、CSP 和通信风险## Worker 不是法外之地
很多人以为 Worker 跑在独立线程里,安全性就天然有保障。恰恰相反——Worker 引入了新的攻击面:跨域脚本加载、postMessage 注入、SharedArrayBuffer 竞态,每一个都可能被利用。本文把 Web Worker 相关的安全问题和防御手段讲清楚。
## 同源策略:第一道防线
Worker 脚本必须和主页面同源(协议 + 域名 + 端口一致)。这是浏览器强制的,不是建议。
```javascript
// 跨域加载 → 直接报错
new Worker('https://evil.com/worker.js'); // SecurityError
// 同源加载 → 正常
new Worker('/workers/task.js');
```
但同源策略有绕过方式,而这些绕过方式本身就是安全隐患。
### Blob URL 的风险
用 Blob URL 可以绕过同源限制,创建内联 Worker:
```javascript
// 从任意字符串创建 Worker
const code = 'self.onmessage = (e) => { /* ... */ }';
const blob = new Blob([code], { type: 'text/javascript' });
new Worker(URL.createObjectURL(blob));
```
问题在于:如果 `code` 的内容来自用户输入或外部 API,攻击者就能注入任意代码在 Worker 里执行。**永远不要用不受信任的数据构造 Worker 脚本**。
用完后必须 `URL.revokeObjectURL()` 释放,否则内存泄漏。
### importScripts 的跨域加载
Worker 内部可以用 `importScripts()` 加载外部脚本,这个方法**不受同源限制**:
```javascript
// worker.js
importScripts('https://cdn.example.com/lib.js'); // 允许跨域
```
这是个设计选择——Worker 需要加载工具库。但这也意味着如果 CDN 被入侵或者 DNS 被劫持,恶意脚本就跑进了你的 Worker。
防御方式:在服务端配置 `Content-Security-Policy` 的 `script-src` 指令,限制 `importScripts` 能加载哪些来源的脚本。
## CSP 对 Worker 的约束
Worker 有自己的执行上下文,CSP 的约束方式和主页面不同:
- **同源 Worker 脚本**(通过 URL 加载):不受创建它的页面的 CSP 限制
- **Blob/data URL Worker**:继承创建它的页面的 CSP 策略
- **Worker 内的 importScripts**:受 Worker 自身的 CSP 约束(如果有)
这意味着如果你想限制 Worker 的行为,需要给 Worker 脚本的 HTTP 响应也加上 CSP 头:
```
Content-Security-Policy: script-src 'self' cdn.example.com
```
## postMessage 通信安全
postMessage 是 Worker 和主线程唯一的通信通道,也是 XSS 注入的潜在入口。
### 验证消息来源
主线程收到的消息不一定来自你的 Worker。特别是 SharedWorker 和 Service Worker 场景下,多个页面都能发消息:
```javascript
// 主线程:验证消息来源和格式
worker.onmessage = (e) => {
const data = e.data;
// 类型校验
if (typeof data !== 'object' || data === null) return;
if (typeof data.type !== 'string') return;
// 只处理已知的消息类型
const allowedTypes = ['result', 'progress', 'error'];
if (!allowedTypes.includes(data.type)) return;
// 处理消息
handleMessage(data);
};
// Worker 端同理:验证主线程发来的数据
self.onmessage = (e) => {
const data = e.data;
if (!data || typeof data.type !== 'string') return;
// ...
};
```
### 不要直接执行消息里的代码
```javascript
// 危险!永远不要这么做
self.onmessage = (e) => {
eval(e.data.code); // 任意代码执行
new Function(e.data.fn)(); // 同样危险
};
```
看似明显,但在模板引擎或动态逻辑场景里容易踩进去。如果必须根据消息执行不同逻辑,用白名单映射:
```javascript
const handlers = {
sort: (data) => { /* ... */ },
filter: (data) => { /* ... */ },
};
self.onmessage = (e) => {
const handler = handlers[e.data.type];
if (handler) handler(e.data.params);
};
```
## SharedArrayBuffer 的安全门槛
SharedArrayBuffer 允许主线程和 Worker 共享同一块内存,没有序列化开销。但它也带来了竞态条件风险——两个线程同时写同一个内存位置,数据就乱了。
浏览器对 SharedArrayBuffer 有严格的安全要求,服务端必须返回以下两个响应头,否则 `new SharedArrayBuffer()` 直接抛错:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
这两个头不是"建议加",而是**强制要求**。原因是为了防止 Spectre 类的侧信道攻击——没有这些头,恶意页面可以通过 SharedArrayBuffer 读取跨域内存数据。
如果加上 COEP 后你的页面加载第三方资源(图片、脚本)出错了,需要给这些资源的响应加上 `Cross-Origin-Resource-Policy: cross-origin` 头。
## Worker 里能访问什么、不能访问什么
从安全角度看,Worker 的 API 限制本身就是一种防护:
| 能访问 | 不能访问 | 安全意义 |
|--------|----------|----------|
| fetch、WebSocket | document、DOM | 不能直接篡改页面 |
| IndexedDB | localStorage | 避免同步 I/O 竞态 |
| Cache API | window、parent | 隔离全局作用域 |
| Notifications | XMLHttpRequest | 推荐用 fetch 替代 |
| performance | location(只读) | 不能跳转页面 |
这些限制意味着即使 Worker 代码被攻破,攻击者也无法直接操作 DOM 或窃取 localStorage 中的 token。Worker 的攻击半径被刻意缩小了。
## 实际攻击场景
**场景 1:CDN 供应链攻击**。你的 Worker 用 `importScripts('https://cdn.example.com/lib.js')`,CDN 被入侵后恶意代码跑进了 Worker。防御:CSP 限制 script-src,或改用 npm 包 + 打包工具。
**场景 2:postMessage 中间人**。攻击者在页面注入脚本拦截 Worker 通信,篡改消息内容。防御:消息加签名校验,关键字段用加密传输。
**场景 3:Blob Worker 代码注入**。从服务端获取的配置数据直接拼进 Worker 代码字符串,攻击者通过配置接口注入恶意代码。防御:Worker 代码和数据严格分离,用 postMessage 传配置,不拼字符串。
## 安全检查清单
- Worker 脚本是否只从同源加载?如果是 Blob URL,代码来源是否可信?
- `importScripts` 加载的外部脚本是否有 CSP 保护?
- postMessage 通信是否做了类型校验和白名单过滤?
- 有没有用 `eval` 或 `new Function` 执行消息中的代码?
- SharedArrayBuffer 是否配了 COOP/COEP 响应头?
- Worker 脚本 MIME 类型是否为 `text/javascript`?
- Blob URL 用完后是否调用了 `revokeObjectURL`?
服务端2026年5月27日 12:37
Web Worker 实战:让页面不再卡顿## JavaScript 的单线程困局
浏览器里,JS 和 UI 渲染共享同一个线程。这意味着一件事:JS 代码跑多久,页面就卡多久。当你排序 10 万条数据、解析 20MB 的 JSON、或者做复杂的图像运算时,用户看到的不是加载动画,而是冻住的页面——滚动没用,点击没用,连浏览器标签页都显示"无响应"。
Web Worker 就是冲着这个问题来的:给 JS 开一条独立线程,把耗时任务丢过去跑,主线程继续处理 UI。
## Worker 到底是什么
Worker 是浏览器提供的一个独立执行环境,和主线程平级运行。几个关键事实:
- **独立线程**:Worker 有自己的调用栈和事件循环,不会阻塞主线程
- **独立全局对象**:Worker 里没有 `window`,取而代之的是 `self`(`DedicatedWorkerGlobalScope`)
- **不能碰 DOM**:`document`、`element`、`localStorage` 一概不可用
- **只能用消息通信**:`postMessage` 发,`onmessage` 收,数据走结构化克隆
- **同源限制**:Worker 脚本必须和页面同源
## 怎么用
### 创建和通信
```javascript
// 主线程
const worker = new Worker('worker.js');
// 发数据给 Worker
worker.postMessage({ type: 'sort', data: largeArray });
// 接收 Worker 返回的结果
worker.onmessage = (e) => {
console.log('结果:', e.data.result);
};
// 出错处理
worker.onerror = (e) => {
console.error(`Worker 错误: ${e.message} (${e.filename}:${e.lineno})`);
};
// 不用了就关掉
worker.terminate();
```
```javascript
// worker.js
self.onmessage = (e) => {
const { type, data } = e.data;
if (type === 'sort') {
const result = data.sort((a, b) => a - b);
self.postMessage({ result });
}
};
```
### 内联 Worker:不想多一个文件
有时候 Worker 代码很短,单独建文件嫌麻烦。可以用 Blob URL 创建内联 Worker:
```javascript
const code = `
self.onmessage = (e) => {
const result = heavyCalc(e.data);
self.postMessage(result);
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
```
这在单文件组件或沙箱环境里特别好用。
### 多个 Worker 并行
一个 Worker 不够就开多个。浏览器对 Worker 数量没有硬限制,但每个 Worker 都占一个线程,开太多反而有调度开销。通常根据 CPU 核心数来定:
```javascript
const cores = navigator.hardwareConcurrency || 4;
const workers = Array.from({ length: cores }, () => new Worker('worker.js'));
// 把任务分片给多个 Worker
const chunkSize = Math.ceil(data.length / cores);
const results = await Promise.all(
workers.map((worker, i) => {
const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize);
return new Promise((resolve) => {
worker.onmessage = (e) => resolve(e.data.result);
worker.postMessage({ data: chunk });
});
})
);
```
## 什么时候该用 Worker
不是所有耗时操作都需要 Worker。判断标准很简单:**会不会阻塞主线程超过 50ms?** 会就上 Worker,不会就不必。
**值得用 Worker 的场景**:
- 大数据排序、过滤、聚合(超过 1 万条记录的客户端处理)
- 文件解析(CSV、JSON、Excel)
- 图像处理(Canvas 像素操作、滤镜)
- 加密运算(RSA、AES 大数据量加密)
- 实时数据流处理(WebSocket 推送数据的聚合计算)
**不需要 Worker 的场景**:
- fetch 请求——本来就异步,不阻塞主线程
- 简单的 DOM 操作——Worker 做不了
- 定时器——setTimeout/setInterval 本身不阻塞
- 少量数据运算(几百条数据的遍历)
## Worker 的限制和绕过方式
| 限制 | 绕过方式 |
|------|----------|
| 不能访问 DOM | 把计算结果 postMessage 回主线程,主线程操作 DOM |
| 不能用 localStorage | 用 IndexedDB 替代,Worker 可以访问 |
| 不能用 XMLHttpRequest | 用 fetch 替代,Worker 支持 |
| 不能用 window 对象 | 用 self 替代全局对象 |
| 同源限制 | 用 Blob URL 创建内联 Worker |
| 通信有序列化开销 | 大数据用 Transferable 零拷贝,高频通信用 SharedArrayBuffer |
## Worker 的三种类型
**Dedicated Worker**:最常见的,和一个页面绑定,页面关了 Worker 也销毁。
**Shared Worker**:多个页面共享同一个 Worker 实例。适合多标签页同步状态的场景,比如购物车数量、未读消息数。创建方式不同:
```javascript
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = (e) => { /* 收消息 */ };
worker.port.postMessage({ type: 'sync' });
```
**Service Worker**:本质是网络代理,拦截请求、管理缓存。PWA 的核心,和普通 Worker 用途完全不同,别混为一谈。
## 常见踩坑
**坑 1:频繁通信拖垮性能**。每秒 postMessage 几百次,序列化开销比计算本身还大。解决方案:批量发送,攒够一批再传;或者改用 SharedArrayBuffer 共享内存。
**坑 2:Worker 里抛的异常主线程收不到**。必须在主线程监听 `worker.onerror`,否则 Worker 静默挂掉你都不知道。
**坑 3:Transferable 传完后原数据变空**。`postMessage({ buffer }, [buffer])` 之后,主线程的 `buffer.byteLength` 变成 0。如果主线程还需要这个数据,先拷贝一份再传。
**坑 4:Worker 脚本路径是相对 HTML 的**,不是相对 JS 文件的。在打包工具(Webpack/Vite)里容易路径搞错,建议用 `new URL('./worker.js', import.meta.url)` 让打包工具正确处理。
```javascript
// Vite/Webpack 5 的正确写法
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' }
);
```
## 性能实测
在 Chrome 120 / M1 MacBook Pro 上,对 100 万元素数组做排序:
| 方案 | 耗时 | 主线程影响 |
|------|------|-----------|
| 主线程直接排序 | ~800ms | UI 完全卡死 |
| Worker 排序 | ~800ms | UI 正常响应 |
| 4 个 Worker 分片排序 | ~250ms | UI 正常响应 |
Worker 不加速计算,但释放主线程。多 Worker 并行才是真正的加速——代价是代码复杂度上去了,需要分片和合并结果。
服务端2026年5月27日 12:32
Web Worker 通信全解析:从 postMessage 到共享内存## 两种通信方式:拷贝和共享
Worker 和主线程之间不共享内存(SharedArrayBuffer 除外),数据必须"过桥"。过桥有两种方式:
**结构化克隆**(默认):数据完整拷贝一份,双方各持一份,互不影响。类似你复印一份文件给同事。
**Transferable 转移**:数据所有权直接移交,发送方丧失访问权。类似你把原件直接递给同事,自己手里没了。
```javascript
// 结构化克隆(默认)—— 数据拷贝
worker.postMessage({ data: largeArray });
// 主线程和 Worker 各有一份,largeArray 仍在
// Transferable 转移 —— 所有权移交
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage({ buffer }, [buffer]);
// buffer.byteLength === 0,主线程不能再用了
```
选哪种?小数据无所谓,大数据(超过 100KB 的 ArrayBuffer、Blob)用 Transferable,否则拷贝开销能吃掉你 Worker 带来的全部性能收益。
## 结构化克隆支持什么
`postMessage` 不是 JSON.stringify,它用的是浏览器内置的结构化克隆算法,能处理的东西比 JSON 多:
能传的:对象、数组、字符串、数字、布尔值、Date、RegExp、Blob、File、ArrayBuffer、TypedArray、Map、Set、ImageData、Error
不能传的:函数、DOM 节点、Symbol、有循环引用的对象(部分情况)
一个容易踩的坑:**对象的方法和原型链不会被克隆**。你传一个 class 实例过去,对面收到的是一个纯数据对象,方法全丢了。如果 Worker 需要调用方法,要么传纯数据重新构造,要么用 RPC 模式。
## 双向通信的实战写法
简单的 echo 通信谁都会写,但生产环境里你需要的是"请求-响应"模式——主线程发任务,Worker 算完回结果,最好还能 Promise 化。
```javascript
// 主线程:封装 RPC 风格的 Worker 通信
class WorkerRPC {
constructor(url) {
this.worker = new Worker(url);
this.id = 0;
this.pending = new Map();
this.worker.onmessage = (e) => {
const { id, result, error } = e.data;
const { resolve, reject } = this.pending.get(id);
this.pending.delete(id);
error ? reject(new Error(error)) : resolve(result);
};
}
call(method, params) {
return new Promise((resolve, reject) => {
const id = ++this.id;
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, method, params });
});
}
}
// 使用
const rpc = new WorkerRPC('worker.js');
const sorted = await rpc.call('sort', { data: largeArray });
```
```javascript
// worker.js:处理 RPC 调用
const handlers = {
sort: ({ data }) => data.sort((a, b) => a - b),
filter: ({ data, condition }) => data.filter(condition),
};
self.onmessage = async (e) => {
const { id, method, params } = e.data;
try {
const result = await handlers[method](params);
self.postMessage({ id, result });
} catch (err) {
self.postMessage({ id, error: err.message });
}
};
```
这样主线程就可以 `await rpc.call('sort', data)` 了,比裸写 `postMessage` + `onmessage` 干净很多。
## SharedArrayBuffer:真正的共享内存
结构化克隆和 Transferable 本质上还是"传数据",有拷贝或转移开销。如果你要的是两个线程同时读写同一块内存,用 SharedArrayBuffer。
```javascript
// 主线程:创建共享内存
const shared = new SharedArrayBuffer(1024);
const view = new Int32Array(shared);
worker.postMessage({ shared });
// Worker:直接读写同一块内存
self.onmessage = (e) => {
const view = new Int32Array(e.data.shared);
// 用 Atomics 做原子操作,避免竞态
Atomics.add(view, 0, 1);
Atomics.store(view, 1, 42);
};
```
**关键点**:共享内存没有自动同步机制,必须用 `Atomics` API 做原子操作,否则两个线程同时写一个位置,数据就乱了。Atomics 提供了 `add`、`sub`、`compareExchange`、`wait`/`notify` 等操作,基本够用。
注意:SharedArrayBuffer 有安全限制,服务端必须返回 `Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp` 两个响应头,否则浏览器直接拒绝。很多开发者在本地调试时发现能用,部署到生产环境就不行,就是这个头没配。
## 其他通信通道
除了 `postMessage`,还有几个不太常见但特定场景好用的通信方式:
**MessageChannel**:创建一对互相连接的端口,可以传给 Worker 作为私有通道。适合多个 Worker 之间直接通信,不经过主线程中转。
```javascript
const channel = new MessageChannel();
worker1.postMessage({ port: channel.port1 }, [channel.port1]);
worker2.postMessage({ port: channel.port2 }, [channel.port2]);
// 两个 Worker 现在可以直接通信了
```
**BroadcastChannel**:同源下所有标签页和 Worker 都能收发的广播通道。适合跨标签页同步状态。
```javascript
const bc = new BroadcastChannel('app-sync');
bc.postMessage({ type: 'data-updated', payload: newData });
bc.onmessage = (e) => { /* 收到其他页面的广播 */ };
```
## 通信性能的实际影响
很多人以为 Worker 通信开销可以忽略,实际上结构化克隆的耗时跟数据量正相关。实测数据:
| 数据量 | 结构化克隆耗时 | Transferable 耗时 |
|--------|---------------|-------------------|
| 10KB | ~0.1ms | ~0.05ms |
| 1MB | ~5ms | ~0.1ms |
| 10MB | ~50ms | ~0.2ms |
| 100MB | ~500ms | ~0.5ms |
数据量越大,结构化克隆越慢,Transferable 优势越明显。10MB 以上的数据,不用 Transferable 基本等于白用 Worker——拷贝时间比计算时间还长。
**实践建议**:如果 Worker 间通信频率高(每秒几十次以上),即使单次数据量小,也要考虑 SharedArrayBuffer + Atomics,省掉反复序列化的开销。
## 错误处理别忘了
Worker 内部抛出的异常不会冒泡到主线程,必须显式监听:
```javascript
worker.onerror = (e) => {
console.error('Worker 出错了:', e.message);
console.error('文件:', e.filename, '行号:', e.lineno);
// 可以选择重新创建 Worker
};
// Worker 内部也要处理异常
self.onmessage = (e) => {
try {
const result = riskyOperation(e.data);
self.postMessage({ id: e.data.id, result });
} catch (err) {
self.postMessage({ id: e.data.id, error: err.message });
}
};
```
生产环境里 Worker 挂了不重启,等于你的后台任务全停了。建议封装一个自动重启的 Worker 管理器:onerror 触发后 terminate 旧 Worker,new 一个新的,再把未完成的任务重放一遍。
服务端2026年5月27日 12:27
Web Worker vs WebAssembly:线程和速度是两码事## 一句话搞清楚
Web Worker 解决的是"线程"问题——把 JavaScript 搬到后台跑,不卡 UI;WebAssembly 解决的是"速度"问题——让浏览器跑接近原生性能的代码。它俩不是竞争关系,更像是搭档:Worker 出线程,WASM 出算力,加在一起才是完整方案。
## 核心区别
| 维度 | Web Worker | WebAssembly |
|------|-----------|-------------|
| 解决什么问题 | JavaScript 单线程阻塞 | JavaScript 性能天花板 |
| 运行环境 | 独立线程,仍是 JS 引擎 | 沙箱虚拟机,跑二进制指令 |
| 语言 | 只能写 JavaScript | C/C++、Rust、Go 等编译而来 |
| 性能天花板 | 和主线程 JS 一样 | 接近原生(通常快 5-20 倍) |
| DOM 访问 | 不行,靠 postMessage 中转 | 不行,同样靠 JS 桥接 |
| 浏览器 API | fetch、IndexedDB、WebSocket 等 | 几乎没有,全靠 JS 胶水代码 |
| 通信成本 | 结构化克隆(深拷贝),可用 Transferable 零拷贝 | 调用 JS 函数,有上下文切换开销 |
| 适用场景 | I/O 密集、后台任务、并发处理 | 计算密集、图像/音视频/加密/物理引擎 |
简单说:**Worker 是多线程方案,WASM 是加速方案**。你选哪个,取决于瓶颈在哪——是"主线程太忙"还是"JS 跑得不够快"。
## 什么时候用 Web Worker
主线程被卡了,就用 Worker。最常见的信号:页面操作出现明显延迟,Chrome DevTools 的 Performance 面板里看到长任务(Long Tasks)超过 50ms。
典型场景:
**大列表排序/过滤**。前端拿到 10 万条数据做客户端筛选,主线程直接冻住。丢给 Worker 后,筛选完把结果 postMessage 回来,UI 全程流畅。
**文件处理**。用户上传 CSV/JSON 大文件,在 Worker 里解析、校验、转换格式,主线程只负责显示进度条。
**实时数据流**。WebSocket 推过来的行情数据,Worker 负责解包、聚合、计算指标,主线程只做渲染。
```javascript
// 主线程
const worker = new Worker('data-worker.js');
// 大数据丢给 Worker 处理,用 Transferable 避免拷贝开销
const buffer = new Float64Array(1_000_000);
worker.postMessage({ data: buffer }, [buffer.buffer]);
worker.onmessage = (e) => {
// 拿到处理结果,更新 UI
renderChart(e.data.result);
};
```
注意 Transferable Objects 的用法:`postMessage` 的第二个参数传 `[buffer.buffer]`,数据直接转移所有权而不是拷贝,大数据场景下差距巨大。
## 什么时候用 WebAssembly
JS 算不过来了,就用 WASM。典型信号:计算密集循环在 Profiler 里占了大量时间,而且算法本身已经是 O(n log n) 级别,没法再优化了。
典型场景:
**图像处理**。给图片加滤镜、裁剪、缩放,像素级操作在 JS 里慢得感人。用 Rust 或 C 写 WASM 模块,处理速度能提升 5-10 倍。
**加密/解密**。AES-256 加密 100MB 数据,JS 版本可能要好几秒,WASM 版本几百毫秒搞定。
**物理引擎/游戏**。碰撞检测、粒子系统,每帧都要大量浮点运算,WASM 是刚需。
**音视频编解码**。FFmpeg 编译成 WASM 在浏览器里跑,已经是很成熟的方案了。
```javascript
// 加载 WASM 模块
const response = await fetch('image-processor.wasm');
const { instance } = await WebAssembly.instantiateStreaming(response);
// 调用导出函数
const imageData = ctx.getImageData(0, 0, width, height);
const ptr = instance.exports.process(imageData.data, width, height);
```
WASM 最大的限制是它不能直接调浏览器 API。你需要写 JS 胶水代码(glue code)来桥接,比如 WASM 算完结果后通过共享内存传给 JS,JS 再操作 DOM 或 Canvas。
## 两者结合:Worker 里跑 WASM
真正高性能的 Web 应用,往往不是二选一,而是**把 WASM 塞进 Worker 里**——Worker 解决线程问题,WASM 解决速度问题,各司其职。
以浏览器端图像处理为例:
```javascript
// 主线程:只管 UI
const worker = new Worker('wasm-image-worker.js');
function processImage(file) {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// 把像素数据转移到 Worker(零拷贝)
worker.postMessage({ pixels: imageData.data.buffer, width: img.width, height: img.height }, [imageData.data.buffer]);
};
img.src = URL.createObjectURL(file);
}
worker.onmessage = (e) => {
// 拿到处理后的像素,渲染到 Canvas
const { pixels, width, height } = e.data;
const newImageData = new ImageData(new Uint8ClampedArray(pixels), width, height);
ctx.putImageData(newImageData, 0, 0);
};
```
```javascript
// wasm-image-worker.js:加载 WASM + 执行计算
let wasm = null;
self.onmessage = async (e) => {
if (!wasm) {
const { instance } = await WebAssembly.instantiateStreaming(fetch('filter.wasm'));
wasm = instance.exports;
}
const { pixels, width, height } = e.data;
// 在 WASM 里处理像素
const resultPtr = wasm.applyFilter(pixels, width, height);
const result = new Uint8Array(wasm.memory.buffer, resultPtr, width * height * 4);
// 结果传回主线程
self.postMessage({ pixels: result.buffer, width, height }, [result.buffer]);
};
```
这个架构的好处:主线程零负担,Worker 线程跑 WASM 接近原生速度,数据通过 Transferable 零拷贝传递。三重优化叠加,效果远超单独用任何一种。
## 性能差异有多大
实际测一下才有体感。以"100 万元素数组求平方根"为例:
| 方案 | 耗时(近似) |
|------|-------------|
| 主线程 JS | ~500ms(期间 UI 卡死) |
| Worker + JS | ~500ms(UI 不卡,但计算一样慢) |
| Worker + WASM | ~50ms(UI 不卡,计算快 10 倍) |
数据来源:Chrome 120,M1 MacBook Pro,具体数值因硬件和算法而异,但量级关系稳定。
关键点:**Worker 不加速计算,只解放主线程;WASM 才是加速计算的**。如果你把慢代码移到 Worker 里,它还是一样慢,只是不卡 UI 了。要真正快,得用 WASM。
## 选择决策
别纠结,按这个思路来:
1. **主线程卡不卡?** 卡 → 上 Worker
2. **Worker 里的计算够不够快?** 不够 → 把热点函数编译成 WASM
3. **两者都不需要?** 那就别用,引入 Worker 有通信开销,WASM 有编译和加载成本
一个常见的误区是"用了 Worker 就快了"——不是的,Worker 只是换个地方跑,JS 该慢还是慢。另一个误区是"WASM 能替代 JS"——也不是,WASM 搞不了 DOM、调不了 fetch、处理不了事件循环,离了 JS 胶水代码它寸步难行。
选对工具,别选"更高级"的。
前端2月21日 15:44
OffscreenCanvas 如何在 Web Worker 中进行渲染?OffscreenCanvas 是 HTML5 提供的一个功能,允许在 Web Worker 中进行 Canvas 渲染,从而将复杂的图形计算从主线程移到后台线程。
## OffscreenCanvas 的核心概念
### 特点
- 可以在 Worker 中进行 Canvas 绘图操作
- 支持大部分 Canvas 2D API 和 WebGL API
- 通过 `transferControlToOffscreen()` 方法将 Canvas 控制权转移
- 适用于复杂的图形渲染和动画
## 基本使用
### 主线程设置
```javascript
// 获取 Canvas 元素
const canvas = document.getElementById('myCanvas');
// 将 Canvas 控制权转移到 OffscreenCanvas
const offscreen = canvas.transferControlToOffscreen();
// 创建 Worker
const worker = new Worker('canvas-worker.js');
// 将 OffscreenCanvas 发送给 Worker
worker.postMessage({ canvas: offscreen }, [offscreen]);
```
### Worker 中渲染
```javascript
// canvas-worker.js
self.onmessage = function(e) {
const canvas = e.data.canvas;
const ctx = canvas.getContext('2d');
// 在 Worker 中进行绘图
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制图形
ctx.fillStyle = 'blue';
ctx.fillRect(50, 50, 100, 100);
// 继续动画
requestAnimationFrame(render);
}
render();
};
```
## 实际应用场景
### 1. 复杂动画渲染
```javascript
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('animation-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// animation-worker.js
self.onmessage = function(e) {
const canvas = e.data.canvas;
const ctx = canvas.getContext('2d');
let particles = [];
function initParticles() {
for (let i = 0; i < 1000; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 2,
vy: (Math.random() - 0.5) * 2,
size: Math.random() * 3 + 1
});
}
}
function updateParticles() {
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
});
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(100, 150, 255, 0.7)`;
ctx.fill();
});
updateParticles();
requestAnimationFrame(render);
}
initParticles();
render();
};
```
### 2. 图像处理
```javascript
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('image-worker.js');
// 加载图像
const img = new Image();
img.onload = function() {
worker.postMessage({
canvas: offscreen,
image: img
}, [offscreen]);
};
img.src = 'image.jpg';
// image-worker.js
self.onmessage = function(e) {
const canvas = e.data.canvas;
const ctx = canvas.getContext('2d');
const img = e.data.image;
// 设置 Canvas 大小
canvas.width = img.width;
canvas.height = img.height;
// 绘制原始图像
ctx.drawImage(img, 0, 0);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 图像处理:灰度化
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // R
data[i + 1] = avg; // G
data[i + 2] = avg; // B
}
// 放回处理后的图像
ctx.putImageData(imageData, 0, 0);
};
```
### 3. WebGL 渲染
```javascript
// 主线程
const canvas = document.getElementById('glCanvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('webgl-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// webgl-worker.js
self.onmessage = function(e) {
const canvas = e.data.canvas;
const gl = canvas.getContext('webgl');
// WebGL 初始化代码
const vertexShaderSource = `
attribute vec4 aVertexPosition;
void main() {
gl_Position = aVertexPosition;
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
// 编译着色器
const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
// 创建程序
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
// 渲染
function render() {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(shaderProgram);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
}
render();
};
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
```
## 与主线程交互
### 动态调整 Canvas 大小
```javascript
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('canvas-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// 监听窗口大小变化
window.addEventListener('resize', function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
worker.postMessage({
type: 'resize',
width: canvas.width,
height: canvas.height
});
});
// canvas-worker.js
self.onmessage = function(e) {
if (e.data.type === 'resize') {
canvas.width = e.data.width;
canvas.height = e.data.height;
}
};
```
### 接收用户输入
```javascript
// 主线程
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('canvas-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
// 发送鼠标位置
canvas.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
worker.postMessage({
type: 'mousemove',
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
});
// canvas-worker.js
let mouseX = 0, mouseY = 0;
self.onmessage = function(e) {
if (e.data.type === 'mousemove') {
mouseX = e.data.x;
mouseY = e.data.y;
}
};
```
## 注意事项
### 1. Canvas 控制权只能转移一次
```javascript
// ❌ 错误:多次转移
const offscreen1 = canvas.transferControlToOffscreen();
const offscreen2 = canvas.transferControlToOffscreen(); // 报错
// ✅ 正确:只转移一次
const offscreen = canvas.transferControlToOffscreen();
```
### 2. OffscreenCanvas 不支持所有 Canvas API
```javascript
// ❌ 不支持
canvas.toDataURL(); // 在 Worker 中不可用
canvas.toBlob(); // 在 Worker 中不可用
// ✅ 使用 ImageBitmap 代替
const bitmap = await createImageBitmap(canvas);
```
### 3. 浏览器兼容性
```javascript
// 检查浏览器支持
if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) {
// 支持 OffscreenCanvas
} else {
// 不支持,使用回退方案
}
```
## 性能优化
### 1. 批量绘制
```javascript
// ❌ 频繁调用绘制方法
for (let i = 0; i < 1000; i++) {
ctx.beginPath();
ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);
ctx.fill();
}
// ✅ 批量绘制
ctx.beginPath();
for (let i = 0; i < 1000; i++) {
ctx.moveTo(particles[i].x, particles[i].y);
ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);
}
ctx.fill();
```
### 2. 使用 ImageBitmap
```javascript
// 加载图像为 ImageBitmap
const bitmap = await createImageBitmap(image);
// 在 Worker 中绘制
ctx.drawImage(bitmap, 0, 0);
```
### 3. 降低渲染频率
```javascript
let lastRenderTime = 0;
const targetFPS = 30;
const frameInterval = 1000 / targetFPS;
function render(timestamp) {
if (timestamp - lastRenderTime >= frameInterval) {
// 执行渲染
ctx.clearRect(0, 0, canvas.width, canvas.height);
// ... 绘制代码
lastRenderTime = timestamp;
}
requestAnimationFrame(render);
}
```
## 最佳实践
1. **复杂渲染使用 OffscreenCanvas**:将计算密集型图形渲染移到 Worker
2. **合理控制渲染频率**:避免不必要的重绘
3. **批量处理**:减少绘制调用次数
4. **使用 ImageBitmap**:提高图像加载和渲染性能
5. **检查浏览器兼容性**:提供回退方案
6. **及时释放资源**:使用完毕后清理资源服务端2月21日 15:24
Service Worker 的生命周期是什么,如何实现离线缓存?Service Worker 是一种特殊的 Web Worker,它作为网络代理运行在浏览器和服务器之间,提供离线功能、推送通知和后台同步等能力。
## Service Worker 的核心概念
### 特点
- 独立于页面生命周期运行
- 拦截和处理网络请求
- 必须在 HTTPS 环境下运行(localhost 除外)
- 可以实现离线缓存和资源预加载
- 支持推送通知和后台同步
## 注册 Service Worker
```javascript
// 检查浏览器支持
if ('serviceWorker' in navigator) {
// 注册 Service Worker
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('Service Worker 注册成功:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker 注册失败:', error);
});
}
```
## Service Worker 生命周期
### 1. Install(安装)
```javascript
// service-worker.js
const CACHE_NAME = 'my-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js',
'/images/logo.png'
];
self.addEventListener('install', function(event) {
// event.waitUntil 延迟安装完成
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('已打开缓存');
return cache.addAll(urlsToCache);
})
);
// 立即激活新的 Service Worker
self.skipWaiting();
});
```
### 2. Activate(激活)
```javascript
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
// 删除旧版本的缓存
if (cacheName !== CACHE_NAME) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// 立即控制所有客户端
self.clients.claim();
});
```
### 3. Fetch(拦截请求)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 缓存命中,返回缓存
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request).then(
function(response) {
// 检查是否为有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应(响应流只能使用一次)
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
```
## 缓存策略
### 1. Cache First(缓存优先)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
return response || fetch(event.request);
})
);
});
```
### 2. Network First(网络优先)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request)
.then(function(response) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(function() {
return caches.match(event.request);
})
);
});
```
### 3. Network Only(仅网络)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(fetch(event.request));
});
```
### 4. Cache Only(仅缓存)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request));
});
```
### 5. Stale While Revalidate(缓存优先,后台更新)
```javascript
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(cachedResponse) {
const fetchPromise = fetch(event.request).then(function(networkResponse) {
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});
```
## 推送通知
### 订阅推送
```javascript
// 主线程
function subscribeUser() {
return navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'BEl62iUYgUivxIkv69yViEuiBIa-IbRMhKDbjjVdMlzQJd0_...'
)
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function(pushSubscription) {
console.log('已接收推送订阅:', pushSubscription);
return pushSubscription;
});
}
```
### 处理推送消息
```javascript
// service-worker.js
self.addEventListener('push', function(event) {
const options = {
body: event.data ? event.data.text() : '新消息',
icon: '/images/icon.png',
badge: '/images/badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
};
event.waitUntil(
self.registration.showNotification('推送通知', options)
);
});
// 处理通知点击
self.addEventListener('notificationclick', function(event) {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});
```
## 后台同步
### 注册同步事件
```javascript
// 主线程
navigator.serviceWorker.ready.then(function(registration) {
registration.sync.register('sync-messages');
});
```
### 处理同步事件
```javascript
// service-worker.js
self.addEventListener('sync', function(event) {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
function syncMessages() {
return fetch('/api/sync-messages', {
method: 'POST',
body: JSON.stringify(getPendingMessages())
});
}
```
## 调试 Service Worker
### Chrome DevTools
1. 打开 Chrome DevTools
2. 切换到 "Application" 面板
3. 左侧选择 "Service Workers"
4. 可以查看状态、更新、注销等操作
### 更新 Service Worker
```javascript
// 手动更新
navigator.serviceWorker.ready.then(function(registration) {
registration.update();
});
// 监听更新
navigator.serviceWorker.addEventListener('controllerchange', function() {
console.log('Service Worker 已更新');
window.location.reload();
});
```
## 最佳实践
1. **HTTPS 要求**:生产环境必须使用 HTTPS
2. **缓存版本管理**:使用版本号管理缓存
3. **错误处理**:添加完善的错误处理
4. **渐进增强**:确保在不支持 Service Worker 的浏览器中也能正常工作
5. **资源清理**:及时清理旧版本的缓存
6. **性能优化**:合理选择缓存策略
7. **安全性**:验证请求来源,防止 CSRF 攻击