Web Workers 处理线程
Web Workers 是 HTML5 引入的一项重要技术,允许在浏览器后台运行 JavaScript 脚本,而不会阻塞主线程的用户界面交互。这使得开发者能够执行计算密集型任务而不影响页面的响应速度。
概述
Web Workers 的主要类型
- 专用 Worker (Dedicated Worker):仅服务于创建它的页面,是最常用的类型
- 共享 Worker (Shared Worker):可被同一浏览器上下文中的多个页面共享,用于跨页面通信
- Service Worker:特殊的 Worker,用于拦截和处理网络请求,实现离线缓存、推送通知等功能
核心概念
- 独立线程:Web Workers 在独立的线程中运行,与主线程并行执行
- 消息通信:通过
postMessage()和onmessage事件进行线程间通信 - 无 DOM 访问:Worker 不能直接访问 DOM,只能进行计算和数据处理
- 同源限制:Worker 脚本必须与主线程同源(或通过 CORS 配置)
- 生命周期:由主线程创建和控制,可通过
terminate()方法终止 - 数据传递:使用结构化克隆算法传递数据,支持大多数 JavaScript 对象类型
浏览器兼容性
| 浏览器 | 专用 Worker | 共享 Worker | Service Worker |
|---|---|---|---|
| Chrome | ✅ 4+ | ✅ 4+ | ✅ 40+ |
| Firefox | ✅ 3.5+ | ✅ 29+ | ✅ 44+ |
| Safari | ✅ 4+ | ✅ 不支持 | ✅ 11.1+ |
| Edge | ✅ 12+ | ✅ 79+ | ✅ 17+ |
| IE | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
注意:Shared Worker 在 Safari 中不支持,Service Worker 需要 HTTPS 环境(localhost 除外)
适用场景
- ✅ 计算密集型任务:大数据处理、复杂算法计算
- ✅ 图像/视频处理:Canvas 图像处理、视频编码解码
- ✅ 数据解析:JSON 解析、CSV 处理、文件解析
- ✅ 实时数据处理:WebSocket 数据处理、实时分析
- ✅ 后台任务:定时任务、数据同步、日志记录
- ❌ DOM 操作:无法直接操作 DOM
- ❌ 简单任务:对于轻量级任务,Worker 的开销可能大于收益
系统架构
线程模型
Web Workers 采用多线程架构,允许在浏览器主线程之外创建独立的 Worker 线程。这种架构设计实现了真正的并行计算,避免了 JavaScript 单线程模型的限制。
┌─────────────────────────────────────────────────────────────┐
│ 浏览器进程 │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 主线程 (UI) │ │ Worker 线程 1 │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ DOM 操作 │ │ │ │ 计算任务 │ │ │
│ │ │ 事件处理 │◄─┼──────┼─►│ 数据处理 │ │ │
│ │ │ UI 渲染 │ │ │ │ 网络请求 │ │ │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ │ postMessage │ postMessage │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 消息队列 (MessageChannel) │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┐ │
│ │ Worker 线程 2 │ │
│ │ ┌────────────┐ │ │
│ │ │ 图像处理 │ │ │
│ │ │ 文件解析 │ │ │
│ │ └────────────┘ │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘通信架构
Worker 线程与主线程之间采用消息传递机制进行通信,这是一种异步、线程安全的通信方式:
消息传递流程
主线程 Worker 线程
│ │
│ 1. postMessage(data) │
├──────────────────────────────►│
│ │ 2. 序列化数据
│ │ (结构化克隆)
│ │
│ │ 3. 执行计算任务
│ │
│ 4. 反序列化数据 │
│◄──────────────────────────────┤
│ 5. 触发 onmessage 事件 │ 6. postMessage(result)
│ │关键特性
- 数据隔离: 每个线程有独立的内存空间,数据传递时进行复制或转移
- 异步通信: 消息传递是非阻塞的,不会影响线程执行
- 类型支持: 使用结构化克隆算法,支持大多数 JavaScript 数据类型
- 错误传播: Worker 中的错误可以通过
onerror事件传递到主线程
线程生命周期
创建 ──► 运行 ──► 空闲 ──► 终止
│ │ │ │
│ │ │ └─ worker.terminate() / self.close()
│ │ │
│ │ └─ 任务完成,等待新消息
│ │
│ └─ 接收消息,执行任务
│
└─ new Worker(scriptURL)资源占用
| 资源类型 | 主线程 | Worker 线程 |
|---|---|---|
| DOM 访问 | ✅ 完全访问 | ❌ 无法访问 |
| Window 对象 | ✅ 可用 | ❌ 不可用 |
| localStorage | ✅ 可用 | ❌ 不可用 |
| IndexedDB | ✅ 可用 | ✅ 可用 |
| Fetch API | ✅ 可用 | ✅ 可用 |
| WebSocket | ✅ 可用 | ✅ 可用 |
| Canvas | ✅ 可用 | ✅ OffscreenCanvas |
| 内存空间 | 独立 | 独立 |
Worker 类型选型指南
Web Workers 生态中存在多种 Worker 类型,每种都有其特定的使用场景和约束。以下是完整的分类与决策流程:
各类型详细对比
| 特性 | Dedicated Worker | Shared Worker | Service Worker | OffscreenCanvas Worker | Worklet | Wasm Worker |
|---|---|---|---|---|---|---|
| 生命周期 | 随页面销毁 | 所有页面关闭后销毁 | 注册后持久运行 | 随页面销毁 | 浏览器管理 | 随页面销毁 |
| 通信方式 | postMessage | port.postMessage | fetch 事件 / postMessage | Bitmap / postMessage | 输入属性 | postMessage |
| DOM 访问 | ❌ | ❌ | ❌ | 仅 Canvas | ❌ | ❌ |
| 支持数量 | 无硬性上限 | 通常 1 个同源实例 | 1 个 per scope | 多个 | 多个 | 多个 |
| 模块支持 | ES Module ✅ | ES Module ✅ | ES Module ✅ | ES Module ✅ | 部分 | ES Module ✅ |
| 典型场景 | 数据计算 | 跨 Tab 通信 | 离线/PWA | 视频处理 | CSS Houdini | 高性能计算 |
- 90% 的场景使用 Dedicated Worker 即可满足需求
- 需要多标签页实时协作时选择 Shared Worker
- 构建 PWA 应用时必须使用 Service Worker
- 进行视频编解码或复杂图像滤镜时考虑 OffscreenCanvas + Worker
通信机制详解
<h4>002-dedicated-worker-comm.html</h4><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【2】Dedicated Worker 基础通信</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Dedicated Worker postMessage/onmessage 单向+双向通信
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #f0f2f5; color: #333;
min-height: 100vh;
}
.container { max-width: 900px; margin: 0 auto; }
.header {
text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 8px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: white; border-radius: 10px; padding: 24px; margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
.card-title {
font-size: 16px; font-weight: 600; color: #444;
border-left: 4px solid #667eea; padding-left: 12px; margin-bottom: 16px;
}
.info-banner {
background: #e8f4fd; border-radius: 6px; padding: 12px 16px;
font-size: 13px; line-height: 1.6; color: #1565c0; margin-bottom: 16px;
}
/* 通信演示区 */
.comm-area { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 600px) { .comm-area { grid-template-columns: 1fr; } }
.thread-box {
border: 2px solid #e0e0e0; border-radius: 8px; padding: 16px;
min-height: 200px;
}
.thread-box.main-thread { border-color: #667eea; background: #f5f3ff; }
.thread-box.worker-thread { border-color: #48bb78; background: #f0fff4; }
.thread-label {
display: inline-block; padding: 4px 12px; border-radius: 20px;
font-size: 12px; font-weight: 600; margin-bottom: 12px;
}
.thread-label.main { background: #667eea; color: white; }
.thread-label.worker { background: #48bb78; color: white; }
.message-input-group { display: flex; gap: 8px; margin-bottom: 12px; }
.message-input-group input {
flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px;
font-size: 13px;
}
.message-input-group input:focus { outline: none; border-color: #667eea; }
.btn {
padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a67d8; }
.btn-success { background: #48bb78; color: white; }
.btn-success:hover { background: #38a169; }
.btn-danger { background: #fc8181; color: white; }
.btn-danger:hover { background: #e53e3e; }
.message-list {
max-height: 180px; overflow-y: auto; font-size: 12px;
}
.msg-item {
padding: 6px 10px; margin-bottom: 4px; border-radius: 4px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } }
.msg-item.sent { background: #dbeafe; color: #1e40af; text-align: right; }
.msg-item.received { background: #dcfce7; color: #166534; }
.msg-item.system { background: #fef3c7; color: #92400e; font-style: italic; }
.status-bar {
display: flex; gap: 16px; align-items: center; padding: 12px 16px;
background: #f8fafc; border-radius: 6px; font-size: 13px;
}
.status-dot {
width: 10px; height: 10px; border-radius: 50%; display: inline-block;
}
.status-dot.active { background: #48bb78; box-shadow: 0 0 6px #48bb78; }
.status-dot.inactive { background: #cbd5e0; }
.compat-notice {
background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px;
padding: 10px 14px; font-size: 12px; color: #92400e; margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Dedicated Worker 基础通信</h1>
<p>postMessage / onmessage 单向与双向通信演示</p>
</div>
<div class="card">
<div class="card-title">通信原理</div>
<div class="info-banner">
💡 <strong>Dedicated Worker</strong> 是最常用的 Worker 类型,仅服务于创建它的页面。主线程和 Worker 通过
<code>postMessage()</code> 发送消息,通过 <code>onmessage</code> 接收消息,实现单向或双向通信。
</div>
<div class="comm-area">
<!-- 主线程 -->
<div class="thread-box main-thread">
<span class="thread-label main">🖥️ 主线程 (Main Thread)</span>
<div class="message-input-group">
<input type="text" id="mainInput" placeholder="输入消息发送给 Worker..." />
<button class="btn btn-primary" onclick="sendToWorker()">发送 ▶</button>
</div>
<div class="message-list" id="mainMessages"></div>
</div>
<!-- Worker 线程 -->
<div class="thread-box worker-thread">
<span class="thread-label worker">⚙️ Worker 线程 (Worker)</span>
<div style="font-size: 11px; color: #888; margin-bottom: 8px;">Worker 自动回复收到的消息</div>
<div class="message-list" id="workerMessages"></div>
</div>
</div>
<div class="status-bar" style="margin-top: 16px;">
<span>状态:</span>
<span class="status-dot active" id="statusDot"></span>
<span id="statusText">Worker 运行中</span>
<span style="margin-left: auto; color: #888;">
消息计数:<strong id="msgCount">0</strong>
</span>
</div>
<div style="display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap;">
<button class="btn btn-success" onclick="sendPing()">📡 发送 Ping</button>
<button class="btn btn-primary" onclick="sendObject()">📦 发送对象</button>
<button class="btn btn-primary" onclick="sendArray()">📋 发送数组</button>
<button class="btn btn-danger" onclick="terminateWorker()">⛔ 终止 Worker</button>
<button class="btn btn-primary" onclick="recreateWorker()">🔄 重建 Worker</button>
</div>
<div class="compat-notice">
⚠️ <strong>兼容性提示:</strong>Web Workers 在所有现代浏览器中均支持(Chrome 4+, Firefox 3.5+, Safari 4+, Edge 12+)。
IE 不支持 Web Workers。
</div>
</div>
</div>
<script>
// ====== 日志函数 ======
const mainMsgEl = document.getElementById('mainMessages');
const workerMsgEl = document.getElementById('workerMessages');
let msgCount = 0;
function addMessage(element, text, type) {
const div = document.createElement('div');
div.className = `msg-item ${type}`;
div.textContent = `[${new Date().toLocaleTimeString()}] ${text}`;
element.appendChild(div);
element.scrollTop = element.scrollHeight;
}
function updateStatus(text, active) {
document.getElementById('statusText').textContent = text;
document.getElementById('statusDot').className = `status-dot ${active ? 'active' : 'inactive'}`;
}
function incrementCount() {
msgCount++;
document.getElementById('msgCount').textContent = msgCount;
}
// ====== Worker 代码 ======
const workerCode = `
// Worker 接收主线程消息
self.onmessage = function(e) {
const data = e.data;
// 根据不同类型回复
if (data.type === 'ping') {
self.postMessage({
type: 'pong',
message: 'Pong! Worker 收到 Ping',
timestamp: Date.now()
});
} else if (data.type === 'text') {
self.postMessage({
type: 'reply',
message: 'Worker 已收到: "' + data.content + '"',
original: data.content,
timestamp: Date.now()
});
} else if (data.type === 'object') {
self.postMessage({
type: 'reply',
message: 'Worker 收到对象,属性数: ' + Object.keys(data.content).length,
receivedKeys: Object.keys(data.content),
timestamp: Date.now()
});
} else if (data.type === 'array') {
self.postMessage({
type: 'reply',
message: 'Worker 收到数组,长度: ' + data.content.length,
sum: data.content.reduce((a, b) => a + b, 0),
timestamp: Date.now()
});
} else {
self.postMessage({
type: 'reply',
message: 'Worker 收到未知类型消息',
raw: data,
timestamp: Date.now()
});
}
};
self.onerror = function(err) {
self.postMessage({ type: 'error', message: err.message });
};
`;
// ====== 创建 Worker ======
let worker = null;
function createWorker() {
const blob = new Blob([workerCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
worker = new Worker(url);
worker.onmessage = function(e) {
const data = e.data;
incrementCount();
if (data.type === 'pong') {
addMessage(workerMsgEl, `发送: Pong! (${Date.now() - data.timestamp}ms 往返)`, 'sent');
addMessage(mainMsgEl, `收到: ${data.message}`, 'received');
} else if (data.type === 'reply') {
addMessage(workerMsgEl, `回复: ${data.message}`, 'sent');
addMessage(mainMsgEl, `收到: ${data.message}`, 'received');
} else if (data.type === 'error') {
addMessage(mainMsgEl, `错误: ${data.message}`, 'system');
}
};
worker.onerror = function(err) {
addMessage(mainMsgEl, `Worker 错误: ${err.message} (${err.filename}:${err.lineno})`, 'system');
updateStatus('Worker 出错', false);
};
updateStatus('Worker 运行中', true);
addMessage(mainMsgEl, 'Worker 已创建并连接', 'system');
}
createWorker();
// ====== 通信操作 ======
function sendToWorker() {
const input = document.getElementById('mainInput');
const text = input.value.trim();
if (!text || !worker) return;
addMessage(mainMsgEl, `发送: "${text}"`, 'sent');
worker.postMessage({ type: 'text', content: text });
input.value = '';
}
document.getElementById('mainInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendToWorker();
});
function sendPing() {
if (!worker) return;
addMessage(mainMsgEl, '发送: Ping!', 'sent');
worker.postMessage({ type: 'ping' });
}
function sendObject() {
if (!worker) return;
const obj = {
name: '测试对象',
value: Math.random() * 100,
tags: ['html5', 'webworkers', 'demo'],
nested: { a: 1, b: 2 }
};
addMessage(mainMsgEl, `发送: 对象 ${JSON.stringify(obj).substring(0, 50)}...`, 'sent');
worker.postMessage({ type: 'object', content: obj });
}
function sendArray() {
if (!worker) return;
const arr = Array.from({ length: 5 }, () => Math.floor(Math.random() * 100));
addMessage(mainMsgEl, `发送: 数组 [${arr.join(', ')}]`, 'sent');
worker.postMessage({ type: 'array', content: arr });
}
function terminateWorker() {
if (worker) {
worker.terminate();
worker = null;
updateStatus('Worker 已终止', false);
addMessage(mainMsgEl, 'Worker 已被 terminate() 终止', 'system');
}
}
function recreateWorker() {
if (worker) {
worker.terminate();
}
mainMsgEl.innerHTML = '';
workerMsgEl.innerHTML = '';
msgCount = 0;
document.getElementById('msgCount').textContent = '0';
createWorker();
}
</script>
</body>
</html>主线程与 Worker 通信时序
理解主线程和 Worker 之间的完整通信流程是高效使用 Web Workers 的基础。以下时序图展示了从消息发送到响应接收的完整过程:
关键时间节点说明
| 阶段 | 操作 | 耗时影响因素 |
|---|---|---|
| 序列化 | Structured Clone 深拷贝数据 | 数据大小、对象嵌套深度 |
| 队列等待 | 消息在队列中排队 | 当前线程繁忙程度 |
| 反序列化 | 接收端还原对象 | 同序列化 |
| 任务执行 | Worker 内部计算逻辑 | 算法复杂度、数据量 |
数据传递方式对比
不同的数据传递方式在性能、安全性和适用场景上差异显著:
各方式详细对比
| 维度 | Structured Clone | Transferable | SharedArrayBuffer | MessagePort |
|---|---|---|---|---|
| 原理 | 深拷贝数据到新内存 | 所有权转移,零拷贝 | 共享同一块内存 | 双向通信管道 |
| 速度 | 中等(需复制) | 最快(指针移动) | 最快(无传输) | 快(轻量级) |
| 原数据可用性 | ✅ 仍可使用 | ❌ 变为空/不可用 | ✅ 双方均可读写 | N/A |
| 数据大小限制 | 数百 MB | 无限制 | 需配置 COOP/COEP | 无限制 |
| 线程安全性 | 天然安全 | 天然安全 | 需 Atomics 保证 | 天然安全 |
| 适用数据 | 任意可克隆对象 | ArrayBuffer 等 | SharedArrayBuffer | 任意可克隆对象 |
| 浏览器要求 | 全部支持 | 全部支持 | 需跨域隔离头 | 全部支持 |
使用建议
// 场景 1: 小数据量 → Structured Clone(默认)
worker.postMessage({ type: 'config', options: { threshold: 0.5 } })
// 场景 2: 大型二进制数据 → Transferable
const largeBuffer = new ArrayBuffer(10 * 1024 * 1024) // 10MB
worker.postMessage({ buffer: largeBuffer }, [largeBuffer])
// largeBuffer 现在已为 detached 状态,length = 0
// 场景 3: 高频读写共享 → SharedArrayBuffer(需 COOP/COEP)
if (crossOriginIsolated) {
const sab = new SharedArrayBuffer(1024)
const int32View = new Int32Array(sab)
worker.postMessage({ sharedBuffer: sab })
// 双方通过 Atomics 读写 int32View
}
// 场景 4: Worker 间直连 → MessagePort
const channel = new MessageChannel()
workerA.postMessage({ port: channel.port1 }, [channel.port1])
workerB.postMessage({ port: channel.port2 }, [channel.port2])Worker 生命周期状态机
各状态特征说明
| 状态 | 触发条件 | 可否恢复 | 资源占用 |
|---|---|---|---|
| Creating | new Worker() 调用 | - | 已分配但未初始化 |
| Loading | 开始下载脚本 | - | 正在加载脚本文件 |
| Running | 脚本加载并执行成功 | - | 活跃状态,全量资源 |
| Idle | 无待处理消息 | 自动转 Running | 保持活跃,监听中 |
| Terminated | terminate() 强制终止 | ❌ 不可恢复 | 已释放 |
| Closed | self.close() 优雅退出 | ❌ 不可恢复 | 已释放 |
| Error | 加载/执行出错 | ❌ 不可恢复 | 部分已释放 |
terminate()是立即生效的暴力终止,正在执行的代码会被打断,pending 的异步操作会被取消self.close()是优雅退出,Worker 可以在关闭前做清理工作并发送最后一条消息- 一旦进入 Terminated 或 Closed 状态,该 Worker 实例无法重新使用,必须创建新的
专用 Worker
基本使用示例

复杂数据传递示例
Web Workers 使用结构化克隆算法(Structured Clone Algorithm)来传递数据。这个算法支持大多数 JavaScript 对象类型,但有一些限制。
支持的数据类型:
- ✅ 基本类型:
String、Number、Boolean、null、undefined - ✅ 对象:
Object、Array、Date、RegExp - ✅ 集合类型:
Map、Set、ArrayBuffer、TypedArray - ✅
Blob、File、ImageData - ❌
Function、Error、DOM 元素、Symbol
Worker 构造函数参数详解
创建 Worker 实例时,可以传递配置选项来控制 Worker 的行为:
const worker = new Worker(scriptURL, options)参数说明
scriptURL (必需)
- Worker 脚本的 URL 地址
- 必须与主线程同源,或配置 CORS
- 可以是相对路径或绝对路径
options (可选)
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | string | "classic" | Worker 类型:"classic" 或 "module" |
credentials | string | "omit" | 凭证模式:"omit"、"same-origin" 或 "include" |
name | string | "" | Worker 名称,用于调试识别 |
使用示例
Inline Worker 与 Blob Worker
除了从外部文件加载 Worker,还可以使用 Blob URL 或 Data URL 创建内联 Worker。这种方式适用于:
- 单文件应用
- 动态生成 Worker 代码
- 避免额外的网络请求
Blob Worker
Data URL Worker
// 使用 Data URL (不推荐,有大小限制)
const workerCode = "self.onmessage = e => self.postMessage(e.data * 2);"
const dataURL = `data:application/javascript,${encodeURIComponent(workerCode)}`
const worker = new Worker(dataURL)
// 注意: Data URL 在某些浏览器中可能有长度限制使用场景对比
| 创建方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 外部文件 | 代码分离,易于维护,支持模块化 | 需要额外 HTTP 请求 | 生产环境,大型项目 |
| Blob URL | 无需额外请求,可动态生成 | 代码管理复杂,调试困难 | 动态配置,小型应用 |
| Data URL | 简单直接 | 有长度限制,编码开销 | 极简单场景 |
最佳实践
// ✅ 推荐: 使用外部文件 + 模块模式
const worker = new Worker("worker.js", { type: "module" })
// ✅ 推荐: Blob Worker 用于动态场景
function createConfigurableWorker(options) {
const code = generateWorkerCode(options)
const blob = new Blob([code], { type: "application/javascript" })
return new Worker(URL.createObjectURL(blob))
}
// ❌ 避免: 在生产环境中过度使用内联 Worker
// 原因: 代码管理困难,不利于缓存和优化共享 Worker
共享 Worker 可以被同一浏览器上下文中的多个页面共享。主要用于不同的页面通信。在下面的示例中,page1 发送的消息会被 page2 接受
高级特性
1. Transferable Objects(可转移对象)
对于大型二进制数据(如 ArrayBuffer),可以使用 transferable objects 来提高性能,避免数据复制
主线程代码:
const worker = new Worker("worker.js")
// 创建大型 ArrayBuffer
const largeBuffer = new ArrayBuffer(10000000) // 10MB
// 转移所有权(不是复制)
worker.postMessage({ buffer: largeBuffer }, [largeBuffer])
// 注意:转移后主线程不能再使用 largeBufferWorker 线程代码:
self.onmessage = function (event) {
const buffer = event.data.buffer
// 现在拥有 buffer 的所有权
// 可以进行处理...
}2. 异步 API 在 Worker 中的使用
Worker 中可以使用大多数异步 API,如 Fetch API、setTimeout 等
Worker 代码示例:
// 使用 Fetch API
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => {
self.postMessage({ type: "data", payload: data })
})
.catch((error) => {
self.postMessage({ type: "error", message: error.message })
})
// 使用 setTimeout
setTimeout(() => {
self.postMessage({ type: "timeout" })
}, 1000)3. 动态导入模块
Worker 支持动态导入 ES 模块
Worker 代码:
// 动态导入模块
importScripts("utils.js") // 传统方式(同步)
// 或者使用 ES 模块(需要浏览器支持,异步)
import("module.js")
.then((module) => {
module.doSomething()
})
.catch((err) => {
console.error("模块加载失败:", err)
})
// 导入多个脚本(按顺序执行)
importScripts("math.js", "string.js", "utils.js")4. ES Module Workers
传统 Worker 使用 importScripts() 同步加载脚本,而 ES Module Workers 允许使用 import/export 语法,支持静态分析和 Tree Shaking。
创建 ES Module Worker:
const worker = new Worker("worker.mjs", { type: "module" })Worker 内使用 ES Module:
import { processImage } from "./image-utils.js"
import { CONFIG } from "./config.js"
self.onmessage = async (event) => {
const result = await processImage(event.data, CONFIG)
self.postMessage(result)
}ES Module Worker 与 Classic Worker 的区别:
| 特性 | Classic Worker(默认) | ES Module Worker |
|---|---|---|
| 创建方式 | new Worker('worker.js') | new Worker('worker.mjs', { type: 'module' }) |
| 导入方式 | importScripts()(同步阻塞) | import / export(异步,顶层 await 可用) |
| 作用域 | 独立全局作用域 | 独立模块作用域 |
| 严格模式 | 可选 | 始终严格模式 |
| Tree Shaking | 不支持 | 支持(构建工具可优化) |
| 顶层 await | 不支持 | 支持 |
importScripts() | 可用 | 不可用(会抛出错误) |
顶层 await 示例:
const data = await fetch("/api/config").then((r) => r.json())
self.onmessage = (event) => {
console.log("配置已加载:", data)
self.postMessage({ config: data, input: event.data })
}ES Module Workers 是现代 Web 应用的推荐方式,但需要浏览器支持(Chrome 80+、Firefox 114+、Safari 15+)。不支持的环境会回退到 Classic Worker。
5. SharedArrayBuffer 与 Atomics
SharedArrayBuffer 允许主线程和 Worker 共享同一块内存,实现零拷贝的高效数据交换。Atomics 提供原子操作,确保共享内存的线程安全。
基本原理:
主线程 Worker 线程
│ │
├── SharedArrayBuffer ─────┤ ← 同一块内存
│ │
├── Atomics.store() ├── Atomics.load()
└── Atomics.wait() └── Atomics.notify()创建共享内存:
const sharedBuffer = new SharedArrayBuffer(4)
const sharedArray = new Int32Array(sharedBuffer)
const worker = new Worker("worker.js")
worker.postMessage({ buffer: sharedBuffer })Worker 中使用共享内存:
self.onmessage = (event) => {
const sharedArray = new Int32Array(event.data.buffer)
Atomics.store(sharedArray, 0, 42)
Atomics.notify(sharedArray, 0)
}主线程等待 Worker 更新:
Atomics.wait(sharedArray, 0, 0)
console.log("Worker 已更新值:", Atomics.load(sharedArray, 0))Atomics 常用方法:
| 方法 | 说明 |
|---|---|
Atomics.store(arr, idx, val) | 原子写入 |
Atomics.load(arr, idx) | 原子读取 |
Atomics.add(arr, idx, val) | 原子加法 |
Atomics.sub(arr, idx, val) | 原子减法 |
Atomics.compareExchange(arr, idx, old, new) | CAS 操作 |
Atomics.wait(arr, idx, val, timeout?) | 阻塞等待值变化 |
Atomics.notify(arr, idx, count?) | 唤醒等待的线程 |
实战示例:多 Worker 并行计算
const THREAD_COUNT = 4
const DATA_SIZE = 1000000
const sharedBuffer = new SharedArrayBuffer(DATA_SIZE * 4)
const resultArray = new Int32Array(sharedBuffer)
const chunkSize = Math.ceil(DATA_SIZE / THREAD_COUNT)
const workers = []
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker("compute-worker.js")
worker.postMessage({
buffer: sharedBuffer,
offset: i * chunkSize,
length: chunkSize
})
workers.push(worker)
}
workers.forEach((w, i) => {
w.onmessage = () => {
console.log(`Worker ${i} 完成`)
}
})由于 Spectre 漏洞的安全影响,SharedArrayBuffer 要求页面使用跨域隔离(Cross-Origin Isolation):
- 服务器须返回
Cross-Origin-Opener-Policy: same-origin - 服务器须返回
Cross-Origin-Embedder-Policy: require-corp
未配置这些响应头时,SharedArrayBuffer 在部分浏览器中不可用。可通过 self.crossOriginIsolated 检测:
if (self.crossOriginIsolated) {
const sab = new SharedArrayBuffer(1024)
} else {
console.warn("页面未跨域隔离,SharedArrayBuffer 不可用")
}6. MessageChannel(消息通道)
MessageChannel 允许在两个 Worker 之间或 Worker 与主线程之间建立直接通信通道。
7. Worker 池(Worker Pool)
对于需要处理大量任务的场景,可以使用 Worker 池来管理多个 Worker 实例,提高并发处理能力。
// Worker 池实现
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workerScript = workerScript
this.poolSize = poolSize
this.workers = []
this.queue = []
this.activeWorkers = 0
// 初始化 Worker 池
for (let i = 0; i < poolSize; i++) {
this.workers.push({
worker: new Worker(workerScript),
busy: false,
id: i
})
}
}
// 执行任务
execute(task, callback) {
return new Promise((resolve, reject) => {
const workerInfo = this.getAvailableWorker()
if (workerInfo) {
this.runTask(workerInfo, task, resolve, reject)
} else {
// 如果没有可用 Worker,加入队列
this.queue.push({ task, resolve, reject })
}
})
}
// 获取可用的 Worker
getAvailableWorker() {
return this.workers.find((w) => !w.busy)
}
// 运行任务
runTask(workerInfo, task, resolve, reject) {
workerInfo.busy = true
this.activeWorkers++
const cleanup = () => {
workerInfo.busy = false
this.activeWorkers--
// 处理队列中的下一个任务
if (this.queue.length > 0) {
const next = this.queue.shift()
this.runTask(workerInfo, next.task, next.resolve, next.reject)
}
}
workerInfo.worker.onmessage = (event) => {
cleanup()
resolve(event.data)
}
workerInfo.worker.onerror = (error) => {
cleanup()
reject(error)
}
workerInfo.worker.postMessage(task)
}
// 终止所有 Worker
terminate() {
this.workers.forEach((w) => w.worker.terminate())
this.workers = []
this.queue = []
}
}
// 使用示例
const pool = new WorkerPool("worker.js", 4)
// 并发处理多个任务
Promise.all([
pool.execute({ data: [1, 2, 3] }),
pool.execute({ data: [4, 5, 6] }),
pool.execute({ data: [7, 8, 9] }),
pool.execute({ data: [10, 11, 12] })
]).then((results) => {
console.log("所有任务完成:", results)
})示例:图像处理 Worker
<img src="https://zhangzhengyang.oss-cn-beijing.aliyuncs.com/images/202505280313176.gif" alt="iShot_2025-05-28_03.12.46" style="zoom:50%;" />生命周期管理
Web Worker 实例在被创建后会持续运行并监听消息,即使其初始任务已完成,也会保持活动状态直至被显式终止。这种设计确保了 Worker 能随时响应后续消息,但也要求开发者主动管理其生命周期以避免资源浪费。
要终止特定 Web Worker,需在其父页面(创建它的上下文)中调用 terminate() 方法。该方法会立即停止 Worker 的所有执行,释放关联的资源,并终止所有未完成的异步操作
// 创建 Worker 实例
const worker = new Worker("worker.js")
// ...执行任务...
// 终止 Worker
worker.terminate()关键特性:
- 即时生效:调用后 Worker 会立即停止执行,不会等待当前任务完成
- 单向操作:终止后无法恢复,需重新创建实例
- 资源释放:自动清理 Worker 占用的内存和线程资源
- 通信中断:终止后所有 pending 的消息传递将被丢弃
最佳实践:
- 在确定不再需要 Worker 时(如组件卸载、页面关闭前)及时调用
- 对于长时间运行的 Worker,建议实现心跳检测机制
- 在 Worker 内部可以调用
self.close()方法让 Worker 进行优雅退出
// Worker 内部优雅退出
self.onmessage = function (event) {
if (event.data === "terminate") {
// 清理资源
// ...
self.close() // Worker 自己关闭
}
}性能优化建议
-
合理使用 Transferable Objects
- 对于大型
ArrayBuffer、ImageData等,使用 transferable objects 避免复制 - 转移后原对象将无法使用,需要重新创建
- 对于大型
-
避免频繁创建和销毁 Worker
- 对于需要多次使用的 Worker,应该复用而不是每次创建新的
- 使用 Worker 池管理多个 Worker 实例
-
批量处理数据
- 将多个小任务合并为一个大任务,减少消息传递次数
- 使用批处理模式处理数组数据
-
控制 Worker 数量
- 使用
navigator.hardwareConcurrency获取 CPU 核心数 - Worker 数量不应超过 CPU 核心数太多
- 使用
// 获取 CPU 核心数
const cpuCores = navigator.hardwareConcurrency || 4
console.log("CPU 核心数:", cpuCores)
// 根据核心数创建 Worker 池
const poolSize = Math.min(cpuCores, 8) // 最多 8 个- 使用进度反馈
- 对于长时间运行的任务,定期发送进度更新
- 避免主线程长时间等待
// Worker 中发送进度
function processLargeData(data) {
const total = data.length
for (let i = 0; i < total; i++) {
// 处理数据...
// 每处理 10% 发送一次进度
if (i % (total / 10) === 0) {
self.postMessage({
type: "progress",
progress: (i / total) * 100
})
}
}
}调试技巧
-
使用 Chrome DevTools
- 在 Sources 面板中可以找到 Worker 脚本
- 可以在 Worker 脚本中设置断点
- 在 Console 面板中可以看到 Worker 的
console.log输出
-
添加调试标识
- 在 Worker 消息中添加类型标识,便于区分不同消息
- 使用时间戳追踪消息传递
// Worker 中
self.postMessage({
type: "result",
data: result,
timestamp: Date.now(),
workerId: "worker-1"
})- 错误追踪
- 使用
try-catch包装 Worker 代码 - 记录详细的错误堆栈信息
- 使用
self.onmessage = function (event) {
try {
// 处理逻辑
} catch (error) {
self.postMessage({
type: "error",
message: error.message,
stack: error.stack,
data: event.data
})
}
}- 性能监控
- 使用
performance.now()测量 Worker 执行时间 - 监控消息传递的延迟
- 使用
// 主线程
const startTime = performance.now()
worker.postMessage(data)
worker.onmessage = function (event) {
const duration = performance.now() - startTime
console.log(`Worker 执行耗时: ${duration}ms`)
}Web Worker 内置 API
Web Worker 提供一系列专用的全局对象和 API,使其能够在独立线程中执行复杂计算和数据处理任务
线程作用域与通信
| 属性/方法 | 描述 |
|---|---|
self | 表示 Worker 线程自身的全局作用域,类似于主线程中的 window 对象 |
postMessage(message) | 向创建该 Worker 的主线程发送消息 |
onmessage | 用于接收主线程发送的消息的事件处理程序 |
脚本导入
importScripts(urls):用于在 Worker 中导入外部 JavaScript 文件。该方法会同步加载并执行指定的脚本文件
特点:
- 所有导入的脚本必须在同一域下(同源策略)
- 脚本按参数顺序同步执行
- 方法会阻塞直到所有脚本加载并执行完成
- 是 Worker 中唯一可用的脚本导入方式(无法使用
<script>标签)
示例:
// 导入单个脚本
importScripts("utils.js")
// 导入多个脚本(按顺序执行)
importScripts("math-utils.js", "string-utils.js", "api-wrapper.js")Web API 支持
Web Worker 支持大部分 Web API,但有一些限制:
| API 类别 | 支持情况 | 说明 |
|---|---|---|
| 网络请求 | ✅ 支持 | XMLHttpRequest 和 fetch()(现代浏览器) |
| 存储 | ✅ 部分支持 | localStorage 和 sessionStorage 不可用(因无 DOM 访问权限)<br>IndexedDB 可用 |
| 定时器 | ✅ 支持 | setTimeout() 和 setInterval() |
| WebSocket | ✅ 支持 | 可使用 WebSocket API 进行实时通信 |
| 导航 | ❌ 不支持 | 无法访问 window 或 document 对象 |
| File API | ✅ 支持 | FileReader、Blob、File 等 |
| URL API | ✅ 支持 | URL、URLSearchParams 等 |
| Crypto API | ✅ 支持 | crypto.subtle(需要 HTTPS) |
其他可用功能
- 全局对象:
navigator(精简版)、console、JSON - 核心函数:
eval()、isNaN()、isFinite()、parseFloat()、parseInt()等 - 错误处理:
try...catch语句 - 本地对象:可以创建和使用自定义对象、数组、Date 等
- 数学计算:
Math对象的所有方法 - 正则表达式:完整支持
注意事项
- 同源策略:所有导入的脚本必须与 Worker 文件同源
- 同步加载:
importScripts()是同步操作,会阻塞 Worker 执行 - 无 DOM 访问:Worker 无法访问
document、window等 DOM 相关对象 - 存储限制:不能使用
localStorage,但可以使用IndexedDB - 脚本隔离:每个 Worker 有独立的全局作用域
Worker 事件类型
Worker 支持多种事件类型,用于处理不同的场景:
主线程事件
| 事件类型 | 触发时机 | 事件对象属性 | 说明 |
|---|---|---|---|
message | 接收到 Worker 发送的消息 | data: 消息数据<br>origin: 消息来源<br>source: 消息源窗口<br>ports: MessagePort 数组 | 最常用的事件,用于接收 Worker 的处理结果 |
error | Worker 执行出错 | message: 错误消息<br>filename: 错误文件名<br>lineno: 错误行号<br>colno: 错误列号 | Worker 内部的未捕获错误会触发此事件 |
messageerror | 消息反序列化失败 | data: 无法反序列化的数据 | 当 Worker 发送的数据无法被结构化克隆时触发 |
Worker 线程事件
| 事件类型 | 触发时机 | 事件对象属性 | 说明 |
|---|---|---|---|
message | 接收到主线程发送的消息 | data: 消息数据 | 接收主线程传递的数据 |
error | Worker 内部发生错误 | message: 错误消息<br>filename: 错误文件名<br>lineno: 错误行号 | Worker 内部的错误处理 |
messageerror | 消息反序列化失败 | data: 无法反序列化的数据 | 主线程发送的数据无法被结构化克隆时触发 |
install | Service Worker 安装时 | - | 仅 Service Worker |
activate | Service Worker 激活时 | - | 仅 Service Worker |
fetch | 拦截网络请求时 | request: Request 对象 | 仅 Service Worker |
SharedWorker 特有事件
| 事件类型 | 触发时机 | 事件对象属性 | 说明 |
|---|---|---|---|
connect | 新连接建立时 | ports: MessagePort 数组 | 当新的客户端连接到 Shared Worker 时触发 |
MessagePort 事件
| 事件类型 | 触发时机 | 说明 |
|---|---|---|
message | 接收到端口消息 | 用于 MessageChannel 通信 |
messageerror | 消息反序列化失败 | 端口接收的数据无法序列化时触发 |
事件监听方式
// 方式 1: 属性赋值 (推荐用于简单场景)
worker.onmessage = function (event) {
console.log("收到消息:", event.data)
}
// 方式 2: addEventListener (推荐用于复杂场景)
worker.addEventListener("message", function (event) {
console.log("收到消息:", event.data)
})
// 方式 3: 移除事件监听器
function handleMessage(event) {
console.log("收到消息:", event.data)
}
worker.addEventListener("message", handleMessage)
// 需要时移除
worker.removeEventListener("message", handleMessage)事件处理最佳实践
// 完整的事件处理示例
class RobustWorker {
constructor(scriptURL) {
this.worker = new Worker(scriptURL)
this.eventHandlers = new Map()
this.setupEventHandlers()
}
setupEventHandlers() {
// 消息处理
this.worker.addEventListener("message", (event) => {
this.handleMessage(event)
})
// 错误处理
this.worker.addEventListener("error", (error) => {
this.handleError(error)
})
// 消息错误处理
this.worker.addEventListener("messageerror", (error) => {
this.handleMessageError(error)
})
}
handleMessage(event) {
const { type, payload } = event.data
// 查找对应的处理函数
const handler = this.eventHandlers.get(type)
if (handler) {
handler(payload)
} else {
console.warn(`未处理的消息类型: ${type}`)
}
}
handleError(error) {
console.error("Worker 错误:", {
message: error.message,
filename: error.filename,
lineno: error.lineno,
colno: error.colno
})
// 可以在这里实现错误上报
this.reportError(error)
}
handleMessageError(error) {
console.error("消息序列化错误:", error)
// 尝试恢复或通知用户
}
on(eventType, handler) {
this.eventHandlers.set(eventType, handler)
return this // 支持链式调用
}
off(eventType) {
this.eventHandlers.delete(eventType)
return this
}
postMessage(type, payload) {
this.worker.postMessage({ type, payload })
return this
}
reportError(error) {
// 实现错误上报逻辑
}
terminate() {
this.worker.terminate()
this.eventHandlers.clear()
}
}
// 使用示例
const worker = new RobustWorker("worker.js")
worker
.on("data", (data) => {
console.log("处理数据:", data)
})
.on("progress", (progress) => {
console.log(`进度: ${progress}%`)
})
.on("complete", () => {
console.log("任务完成")
worker.terminate()
})
// 发送任务
worker.postMessage("process", { data: [1, 2, 3, 4, 5] })完整示例:
安全性考虑
同源策略
Web Worker 受同源策略约束,确保应用安全性:
- 脚本来源限制
- Worker 脚本必须与主页面同源
- 跨域 Worker 需要 CORS 头配置
- 不能从
file://协议加载 Worker
// ✅ 正确: 同源 Worker
const worker = new Worker("/workers/data-processor.js")
// ✅ 正确: 跨域 Worker (需要 CORS)
const worker = new Worker("https://cdn.example.com/worker.js")
// ❌ 错误: file:// 协议
// const worker = new Worker('worker.js'); // 在本地文件中会失败- 跨域 Worker 配置
数据安全
敏感数据处理
// ❌ 危险: 直接传递敏感数据
// worker.postMessage({
// password: 'user_password',
// apiKey: 'secret_key'
// });
// ✅ 安全: 传递加密后的数据或引用
const encryptedData = await encrypt(sensitiveData)
worker.postMessage({ data: encryptedData })
// ✅ 安全: 使用 token 而不是密码
worker.postMessage({
token: getAuthToken(), // 临时 token
action: "process"
})防止 XSS 攻击
// ❌ 危险: 动态拼接 Worker 代码
// const userCode = getUserInput();
// const worker = new Worker(`data:application/javascript,${userCode}`);
// ✅ 安全: 验证和清理输入
function sanitizeWorkerCode(code) {
// 移除危险的函数调用
const dangerousPatterns = [/eval\s*\(/g, /Function\s*\(/g, /document\./g, /window\./g]
let sanitized = code
dangerousPatterns.forEach((pattern) => {
sanitized = sanitized.replace(pattern, "/* blocked */")
})
return sanitized
}
// 或者完全不使用动态代码消息验证
所有来自 Worker 的消息都应该进行验证:
// 消息验证模式
const messageValidator = {
validate(message, schema) {
for (const [key, type] of Object.entries(schema)) {
if (!(key in message)) {
throw new Error(`Missing required field: ${key}`)
}
if (typeof message[key] !== type) {
throw new Error(`Invalid type for ${key}: expected ${type}`)
}
}
return true
}
}
// 使用示例
worker.onmessage = function (event) {
try {
// 验证消息结构
messageValidator.validate(event.data, {
type: "string",
payload: "object"
})
// 处理消息
handleMessage(event.data)
} catch (error) {
console.error("消息验证失败:", error)
}
}Worker 隔离
Worker 提供天然的安全隔离:
-
独立的执行环境
- 无法访问主线程的变量
- 无法访问 DOM
- 无法访问
localStorage和sessionStorage
-
受限的 API 访问
- 只能使用允许的 Web API
- 不能执行某些敏感操作
-
错误隔离
- Worker 中的错误不会直接导致主线程崩溃
- 需要显式处理错误传播
最佳安全实践
class SecureWorker {
constructor(scriptURL, options = {}) {
// 验证 URL
if (!this.isValidWorkerURL(scriptURL)) {
throw new Error("Invalid worker URL")
}
this.worker = new Worker(scriptURL, options)
this.setupSecurityHandlers()
this.messageSchema = options.messageSchema || {}
}
isValidWorkerURL(url) {
try {
const parsed = new URL(url, window.location.origin)
// 只允许 https 和同源
return parsed.protocol === "https:" || parsed.origin === window.location.origin
} catch {
return false
}
}
setupSecurityHandlers() {
this.worker.onerror = (error) => {
// 阻止错误冒泡
error.preventDefault()
// 记录错误
this.logSecurityEvent("worker_error", {
message: error.message,
filename: error.filename,
lineno: error.lineno
})
}
this.worker.onmessageerror = (error) => {
this.logSecurityEvent("message_error", {
type: error.type
})
}
}
postMessage(data) {
// 验证发送的数据
if (!this.validateOutgoingData(data)) {
throw new Error("Invalid data format")
}
this.worker.postMessage(data)
}
onMessage(handler) {
this.worker.onmessage = (event) => {
// 验证接收的数据
if (this.validateIncomingData(event.data)) {
handler(event.data)
} else {
this.logSecurityEvent("invalid_message", event.data)
}
}
}
validateOutgoingData(data) {
// 实现数据验证逻辑
return true
}
validateIncomingData(data) {
// 实现数据验证逻辑
return true
}
logSecurityEvent(event, data) {
// 记录安全事件
console.warn("Security Event:", event, data)
}
terminate() {
this.worker.terminate()
}
}高级应用场景
1. 加密解密操作
Web Worker 非常适合执行加密解密等计算密集型任务:
2. 数据压缩与解压
3. WebAssembly 集成
Web Worker 可以与 WebAssembly 结合,获得接近原生的性能:
4. 大文件分片处理
构建工具集成:Vite / Webpack Worker 配置
在现代前端工程中,通常使用构建工具(Vite 或 Webpack)来打包项目,Worker 脚本的配置方式与原生开发有所不同。以下介绍主流构建工具中的 Worker 配置方案。
Vite 中的 Worker 配置
Vite 对 Web Workers 提供了开箱即用的支持,推荐使用以下几种方式:
方式一:?worker&inline 查询参数(推荐)
Vite 支持通过查询参数导入 Worker 脚本,这是最简洁的方式:
// 导入 Worker 脚本(返回 Worker 构造函数)
import MyWorker from './worker.js?worker'
const worker = new MyWorker()
worker.postMessage({ data: [1, 2, 3] })
worker.onmessage = (e) => {
console.log('结果:', e.data)
}查询参数说明:
| 参数 | 说明 |
|---|---|
?worker | 将文件作为 Worker 入口打包,导出 Worker 构造函数 |
?worker&inline | 将 Worker 代码内联为 Blob URL,不产生额外请求 |
?worker&url | 返回 Worker 文件的 URL 字符串 |
?sharedworker | 作为 SharedWorker 导入(同理支持 inline/url) |
方式二:new Worker() with { type: 'module' }
如果不想使用导入语法,可以在 Vite 中使用标准的 new Worker() 配合 ES Module 类型:
// vite.config.js 中无需特殊配置即可使用
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' }
)
worker.onmessage = (e) => console.log(e.data)
worker.postMessage({ action: 'start' })注意:
new URL(..., import.meta.url)是 Vite/现代打包器的标准写法,确保开发环境和生产环境中路径都能正确解析。
方式三:Vite 配置项
在 vite.config.js 中可以对 Worker 行为进行更精细的控制:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
worker: {
format: 'es', // Worker 输出格式: 'es' | 'iife' | 'umd'
plugins: [], // Worker 专用的 Vite 插件
rollupOptions: { // Rollup 打包选项
output: {
// Worker chunk 文件命名
entryFileNames: 'assets/workers/[name]-[hash].js'
}
}
}
})Webpack 中的 Worker 配置
Webpack 5 对 Worker 有内置支持,也支持通过插件扩展。
方式一:Webpack 5 内置 Worker(推荐)
Webpack 5 原生支持 new Worker() / new SharedWorker() 构造函数:
// webpack.config.js
module.exports = {
// 开启 Worker 支持(Webpack 5 默认开启)
// 无需额外配置
}
// 源码中使用
const worker = new Worker(
new URL('./worker.js', import.meta.url),
{ type: 'module' }
)方式二:worker-loader(Webpack 4 兼容)
对于旧版 Webpack 或需要更细粒度控制的场景:
// 安装: npm install -D worker-loader
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
inline: 'fallback', // 'no-fallback' | 'fallback' | true
publicPath: '/workers/',
filename: '[name].[hash].worker.js',
esModule: true // 启用 ES Module 导出
}
}
}
]
}
}// 源码中使用
import MyWorker from './my.worker.js'
const worker = new MyWorker()
worker.postMessage('hello')方式三:comlink-loader(基于 RPC 的 Worker 通信)
Comlink 是 Google Chrome Labs 出品的库,可以让 Worker 通信像调用本地函数一样自然:
// 安装: npm install comlink comlink-loader
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.worker\.js$/,
use: ['comlink-loader']
}
]
}
}// worker.js
import * as Comlink from 'comlink'
const obj = {
counter: 0,
async increment(amount) {
this.counter += amount
return this.counter
}
}
export default Comlink.expose(obj)// main.js
import Worker from './worker.js'
import * as Comlink from 'comlink'
const instance = Comlink.wrap(new Worker())
const result = await instance.increment(5)
console.log(result) // 5构建工具对比总结
| 特性 | Vite | Webpack 5 | worker-loader | comlink-loader |
|---|---|---|---|---|
| 导入语法 | import w from './w?worker' | new URL() + new Worker() | import w from './w.worker' | import w from './w.worker' |
| ES Module | ✅ 原生支持 | ✅ 原生支持 | 需配置 | ✅ 支持 |
| HMR | ✅ 支持 | ⚠️ 有限支持 | ❌ 不支持 | ❌ 不支持 |
| 内联选项 | ?inline | 需插件 | inline: true | ❌ |
| SharedWorker | ?sharedworker | ✅ 原生支持 | 需单独配置 | ❌ |
| TypeScript | ✅ 原生支持 | ✅ 原生支持 | 需声明文件 | 需声明文件 |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
- Vite 项目:优先使用
?worker查询参数,简洁且功能完善 - Webpack 5 项目:使用
new URL() + new Worker()标准写法,无需额外依赖 - 需要 RPC 风格通信:考虑 comlink-loader,大幅简化 Worker 通信代码
Web Worker 调试与性能分析
Worker 运行在独立线程中,调试和分析其行为需要专门的技巧和工具。本章介绍如何系统性地调试 Worker 并分析其性能瓶颈。
Chrome DevTools Threads 面板调试
Chrome DevTools 为 Worker 调试提供了完善的可视化支持:
Sources 面板定位 Worker 脚本
- 打开 DevTools → Sources 面板
- 左侧文件树顶部找到 Page 分组下展开 Workers 或 Threads
- 每个 Worker 以独立线程形式列出,点击即可查看源码
- 支持设置断点、单步执行、查看变量——与主线程调试体验一致
Sources 面板结构:
├── Page
│ ├── (index.html) ← 主线程
│ ├── top
│ │ ├── main.js
│ │ └── app.js
│ ├──Workers / Threads
│ │ ├── worker.js:1 ← Dedicated Worker 线程
│ │ ├── compute-worker.js:1 ← 另一个 Worker
│ │ └── shared-worker.js:1 ← Shared Worker
│ └── ...
├── FileSystem
├── Overrides
└── SnippetsConsole 面板的上下文切换
Console 面板默认显示主线程日志。要查看 Worker 的 console.log 输出:
- Console 面板左上角的下拉框中选择目标 Context:
top— 主线程worker.js:1— Worker 线程shared-worker.js:1— Shared Worker 线程
// Worker 中的 console 使用
self.onmessage = (e) => {
console.log('[Worker] 收到:', e.data) // ✅ 正常输出
console.warn('[Worker] 警告: 数据过大') // ✅ 正常输出
console.error('[Worker] 错误:', new Error()) // ✅ 正常输出
console.table([{ id: 1, name: 'a' }]) // ✅ 表格输出
console.time('processing') // ✅ 计时器
// ... 计算 ...
console.timeEnd('processing')
}Console 在 Worker 中的使用限制
| 功能 | Worker 中是否可用 | 备注 |
|---|---|---|
console.log/warn/error | ✅ 可用 | 输出到对应 Context |
console.table | ✅ 可用 | 表格格式化输出 |
console.time/timeEnd | ✅ 可用 | 性能计时 |
console.trace | ✅ 可用 | 打印调用栈 |
console.count/countReset | ✅ 可用 | 计数器 |
console.group/groupEnd | ✅ 可用 | 日志分组 |
console.assert | ✅ 可用 | 条件断言 |
console.clear | ✅ 可用 | 但只清除当前 Context |
console.dir/dirxml | ✅ 可用 | 对象详情 |
console.profile/profileEnd | ⚠️ 有限支持 | 建议用 Performance 面板替代 |
$0 / $(selector) | ❌ 不可用 | 无 DOM 访问权限 |
copy() | ⚠️ 有限支持 | 部分浏览器可用 |
debugger 语句 | ✅ 可用 | 触发断点暂停 |
Performance 面板分析 Worker 性能
Performance 面板可以录制并分析 Worker 线程的运行时性能表现:
录制 Worker 性能
- 打开 DevTools → Performance 面板
- 点击 Record 按钮(或按
Ctrl+E) - 执行触发 Worker 任务的交互操作
- 点击 Stop 结束录制
- 在时间轴中找到 Worker 相关的线程轨道
分析要点
关键指标解读:
| 指标 | 含义 | 优化方向 |
|---|---|---|
| Self Time | 函数自身执行时间(不含子调用) | 定位真正的计算热点 |
| Total Time | 包含子调用的总耗时 | 了解整体调用链路 |
| Idle 时间 | Worker 空闲等待时长 | 考虑合并任务减少唤醒次数 |
| Main Thread Blocking | Worker 是否导致主线程卡顿 | 检查 GC 或大内存分配 |
实战:检测 Worker 中的长任务
// Worker 中添加性能标记
self.onmessage = async (e) => {
// 标记任务开始
performance.mark('task-start')
// 执行计算...
const result = heavyComputation(e.data)
// 标记任务结束
performance.mark('task-end')
performance.measure('worker-task', 'task-start', 'task-end')
// 将性能数据回传给主线程
const entries = performance.getEntriesByName('worker-task')
self.postMessage({
result,
perf: entries[entries.length - 1].duration
})
// 清理标记
performance.clearMarks()
performance.clearMeasures()
}Worker 内存泄漏检测
Worker 拥有独立的内存空间,但其内存泄漏同样会导致页面整体性能下降甚至崩溃。
常见泄漏模式
// ❌ 泄漏模式 1: 未清理的事件监听器
self.onmessage = function(e) {
// 每次 message 都新增一个定时器,从不清理
setInterval(() => {
processData(e.data) // 闭包持有对 e.data 的引用
}, 1000)
}
// ✅ 正确做法: 及时清理
let timerId = null
self.onmessage = function(e) {
if (timerId) clearInterval(timerId) // 先清理旧的
timerId = setInterval(() => {
processData(e.data)
}, 1000)
}
// ❌ 泄漏模式 2: 缓存无限增长
const cache = new Map()
self.onmessage = function(e) {
cache.set(e.data.id, e.data.largePayload) // 永不删除
}
// ✅ 正确做法: LRU 缓存或限制大小
const MAX_CACHE_SIZE = 100
const cache = new Map()
self.onmessage = function(e) {
if (cache.size >= MAX_CACHE_SIZE) {
// 删除最早的条目
const oldestKey = cache.keys().next().value
cache.delete(oldestKey)
}
cache.set(e.data.id, e.data.largePayload)
}
// ❌ 泄漏模式 3: ArrayBuffer 未释放
self.onmessage = function(e) {
const buffers = [] // 全局数组持续增长
buffers.push(e.data.buffer) // 引用不释放
}
// ✅ 正确做法: 用完即弃
self.onmessage = function(e) {
processBuffer(e.data.buffer)
// 局部变量在函数结束后会被 GC 回收
}Memory 面板快照对比
- Performance → Memory 勾选框(或直接打开 Memory 面板)
- 选择 Heap snapshot 类型
- 在 Worker 执行前后分别拍摄快照
- 切换到 Comparison 视图,观察对象增量
- 关注
(array)、(typed array)、Object等类型的 Delta
// 手动触发 GC(仅在 DevTools 中有效)
if (typeof gc !== 'undefined') {
gc() // 强制垃圾回收
}调试最佳实践清单
| 技巧 | 说明 | 适用场景 |
|---|---|---|
| 给 Worker 命名 | new Worker(url, { name: 'compute' }) | 多 Worker 项目快速区分 |
| 消息类型标识 | 每条消息带 type 字段 | Console 中过滤日志 |
| 唯一 ID 追踪 | 每次请求分配 requestId | 异步操作关联请求/响应 |
| 性能埋点 | performance.mark/measure | 生产环境性能监控 |
| 错误边界 | Worker 外层 try-catch + 上报 | 线上问题排查 |
| 心跳机制 | 定期 ping/pong | 检测 Worker 是否存活 |
// 完整的调试增强 Worker 封装
class DebuggableWorker {
constructor(scriptUrl, options = {}) {
this.name = options.name || scriptUrl
this.worker = new Worker(scriptUrl, { ...options, name: this.name })
this.requestId = 0
this.pendingRequests = new Map()
this.setupDebugHandlers()
}
setupDebugHandlers() {
this.worker.onmessage = (e) => {
const { requestId, type, payload, perf } = e.data
// 性能日志
if (perf) {
console.log(`[${this.name}] Task #${requestId} completed in ${perf.toFixed(2)}ms`)
}
// 请求匹配
if (requestId && this.pendingRequests.has(requestId)) {
const { resolve } = this.pendingRequests.get(requestId)
this.pendingRequests.delete(requestId)
resolve(payload)
}
}
this.worker.onerror = (e) => {
console.error(`[${this.name}] Error:`, e.message, `@${e.filename}:${e.lineno}:${e.colno}`)
}
}
execute(payload) {
const id = ++this.requestId
console.log(`[${this.name}] Sending request #${id}`, payload)
return new Promise((resolve) => {
this.pendingRequests.set(id, { resolve })
this.worker.postMessage({ requestId: id, ...payload })
})
}
// 心跳检测
startHeartbeat(interval = 5000) {
this.heartbeatTimer = setInterval(() => {
const start = performance.now()
this.worker.postMessage({ type: '__ping__', ts: start })
const timeout = setTimeout(() => {
console.warn(`[${this.name}] Heartbeat timeout! Worker may be frozen.`)
}, interval * 0.8)
const onPong = (e) => {
if (e.data?.type === '__pong__') {
clearTimeout(timeout)
const latency = performance.now() - e.data.ts
console.log(`[${this.name}] Heartbeat OK, latency: ${latency.toFixed(2)}ms`)
}
}
this.worker.addEventListener('message', onPong, { once: true })
}, interval)
}
terminate() {
clearInterval(this.heartbeatTimer)
this.worker.terminate()
this.pendingRequests.clear()
}
}OffscreenCanvas + Worker 渲染管线
OffscreenCanvas 是 HTML5 Canvas 的离屏版本,可以脱离 DOM 在 Worker 线程中进行渲染操作,然后将渲染结果位图传回主线程显示。这彻底解决了 Canvas 渲染阻塞主线程的问题。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【9】OffscreenCanvas + Worker 渲染</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: OffscreenCanvas transferControlToOffscreen 在 Worker 中渲染 Canvas
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #0d1117; color: #e6edf3;
min-height: 100vh;
}
.container { max-width: 1000px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #238636, #2ea043);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #161b22; border: 1px solid #30363d; border-radius: 10px;
padding: 24px; margin-bottom: 20px;
}
.card-title {
font-size: 15px; font-weight: 600; color: #3fb950;
border-left: 3px solid #3fb950; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(63,185,80,0.1); border: 1px solid rgba(63,185,80,0.25);
border-radius: 6px; padding: 12px 16px; font-size: 13px; line-height: 1.6;
color: #7ee787; margin-bottom: 16px;
}
.canvas-wrapper {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;
}
@media (max-width: 700px) { .canvas-wrapper { grid-template-columns: 1fr; } }
.canvas-box {
background: #0d1117; border: 2px solid #30363d; border-radius: 10px;
padding: 12px; text-align: center;
}
.canvas-label {
font-size: 12px; color: #8b949e; margin-bottom: 8px; font-weight: 600;
}
canvas {
background: #000; border-radius: 6px; display: block; margin: 0 auto;
max-width: 100%;
}
.controls { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-green { background: #238636; color: white; }
.btn-green:hover { background: #2ea043; }
.btn-blue { background: #1f6feb; color: white; }
.btn-blue:hover { background: #388bfd; }
.btn-orange { background: #bd561d; color: white; }
.btn-orange:hover { background: #d18616; }
.btn-purple { background: #8957e5; color: white; }
.btn-purple:hover { background: #a371f7; }
.btn-red { background: #da3633; color: white; }
.btn-red:hover { background: #f85149; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.stats-bar {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
margin-top: 16px;
}
.stat-item {
background: #0d1117; border: 1px solid #21262d; border-radius: 6px;
padding: 10px; text-align: center;
}
.stat-val { font-size: 18px; font-weight: 700; color: #58a6ff; }
.stat-lbl { font-size: 10px; color: #8b949e; margin-top: 2px; }
.log-area {
background: #0d1117; border-radius: 6px; padding: 12px;
font-family: monospace; font-size: 11px; max-height: 150px;
overflow-y: auto; color: #8b949e; line-height: 1.6;
}
.log-entry { padding: 1px 0; }
.log-entry.ok { color: #3fb950; }
.log-entry.err { color: #f85149; }
.log-entry.info { color: #58a6ff; }
.compat-warning {
background: rgba(255,165,2,0.1); border: 1px solid rgba(255,165,2,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffa502;
margin-top: 16px;
}
.support-badge {
display: inline-block; padding: 2px 10px; border-radius: 10px;
font-size: 11px; font-weight: 600; margin-left: 8px;
}
.badge-yes { background: rgba(63,185,80,0.2); color: #3fb950; }
.badge-no { background: rgba(248,81,73,0.2); color: #f85149; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🖼️ OffscreenCanvas + Worker 渲染</h1>
<p>transferControlToOffscreen — 在 Worker 线程中进行 Canvas 绘制,不阻塞主线程</p>
</div>
<div class="card">
<div class="card-title">🎨 Canvas 渲染演示</div>
<div class="info-banner">
💡 <strong>原理:</strong>通过 <code>canvas.transferControlToOffscreen()</code> 将渲染控制权转移给 OffscreenCanvas,
然后在 Worker 中进行绘制操作。主线程完全不参与渲染过程。<br>
支持:
<span class="support-badge badge-yes">Chrome 69+</span>
<span class="support-badge badge-yes">Edge 79+</span>
<span class="support-badge badge-no">Firefox 部分支持</span>
<span class="support-badge badge-no">Safari 不支持</span>
</div>
<div class="controls">
<button class="btn btn-green" onclick="startAnimation()">▶ 开始粒子动画</button>
<button class="btn btn-blue" onclick="drawMandelbrot()">🌀 Mandelbrot 分形</button>
<button class="btn btn-orange" onclick="drawWaveform()">〰️ 波形动画</button>
<button class="btn btn-purple" onclick="drawClock()">🕐 实时时钟</button>
<button class="btn btn-red" onclick="stopAll()">⛹ 停止全部</button>
</div>
<div class="canvas-wrapper">
<div class="canvas-box">
<div class="canvas-label">🖥️ 主线程 Canvas (对比)</div>
<canvas id="mainCanvas" width="400" height="300"></canvas>
</div>
<div class="canvas-box">
<div class="canvas-label">⚙️ Worker OffscreenCanvas</div>
<canvas id="offscreenCanvas" width="400" height="300"></canvas>
</div>
</div>
<div class="stats-bar">
<div class="stat-item">
<div class="stat-val" id="fpsValue">-</div>
<div class="stat-lbl">FPS (Worker)</div>
</div>
<div class="stat-item">
<div class="stat-val" id="particleCount">0</div>
<div class="stat-lbl">粒子数量</div>
</div>
<div class="stat-item">
<div class="stat-val" id="renderMode">空闲</div>
<div class="stat-lbl">当前模式</div>
</div>
<div class="stat-item">
<div class="stat-val" id="workerStatus">就绪</div>
<div class="stat-lbl">Worker 状态</div>
</div>
</div>
<div class="log-area" id="logArea">
<div class="log-entry info">[系统] 等待用户操作... (OffscreenCanvas 需要 Chrome 69+ 或 Edge 79+)</div>
</div>
</div>
<div class="compat-warning">
⚠️ <strong>兼容性提示:</strong>OffscreenCanvas 目前在 Chrome/Edge 中支持最好。Firefox 从 v105 开始部分支持(仅 2D context)。
Safari 暂不支持。如果您的浏览器不支持,将回退到主线程 Canvas 绘制。
</div>
</div>
<script>
const logArea = document.getElementById('logArea');
let offscreenWorker = null;
let animationId = null;
let mainAnimId = null;
function log(msg, type = '') {
const div = document.createElement('div');
div.className = `log-entry ${type}`;
div.textContent = `[${new Date().toTimeString().substring(0,8)}] ${msg}`;
logArea.appendChild(div);
logArea.scrollTop = logArea.scrollHeight;
}
function updateStats(fps, particles, mode, status) {
if (fps !== undefined) document.getElementById('fpsValue').textContent = fps;
if (particles !== undefined) document.getElementById('particleCount').textContent = particles;
if (mode !== undefined) document.getElementById('renderMode').textContent = mode;
if (status !== undefined) document.getElementById('workerStatus').textContent = status;
}
// ====== 检测 OffscreenCanvas 支持情况 ======
const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'
&& typeof HTMLCanvasElement.prototype.transferControlToOffscreen === 'function';
// ====== Worker 渲染代码 ======
const renderWorkerCode = `
let ctx = null;
let animId = null;
let mode = 'idle';
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
// 粒子系统
let particles = [];
function createParticles(count) {
particles = [];
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * 400,
y: Math.random() * 300,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
radius: Math.random() * 4 + 2,
hue: Math.random() * 360
});
}
}
function drawParticles() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
ctx.fillRect(0, 0, 400, 300);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.hue = (p.hue + 1) % 360;
if (p.x < 0 || p.x > 400) p.vx *= -1;
if (p.y < 0 || p.y > 300) p.vy *= -1;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + p.hue + ', 80%, 60%)';
ctx.fill();
});
// 计算 FPS
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = now;
self.postMessage({ type: 'fps', value: fps });
}
if (mode === 'particles') {
animId = requestAnimationFrame(drawParticles);
}
}
function drawMandelbrot() {
const w = 400, h = 300;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
const maxIter = 100;
for (let py = 0; py < h; py++) {
for (let px = 0; px < w; px++) {
const x0 = (px / w) * 3.5 - 2.5;
const y0 = (py / h) * 2 - 1;
let x = 0, y = 0, iter = 0;
while (x*x + y*y <= 4 && iter < maxIter) {
const xtemp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtemp;
iter++;
}
const idx = (py * w + px) * 4;
if (iter === maxIter) {
data[idx] = data[idx+1] = data[idx+2] = 0;
} else {
const t = iter / maxIter;
data[idx] = Math.floor(9 * (1-t) * t * t * t * 255);
data[idx+1] = Math.floor(15 * (1-t) * (1-t) * t * t * 255);
data[idx+2] = Math.floor(8.5 * (1-t) * (1-t) * (1-t) * t * 255);
}
data[idx+3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
self.postMessage({ type: 'done', mode: 'mandelbrot' });
}
function drawWaveform(time) {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 400, 300);
for (let ch = 0; ch < 3; ch++) {
ctx.beginPath();
ctx.strokeStyle = ['hsl(0,80%,60%)','hsl(120,80%,60%)','hsl(240,80%,60%)'][ch];
ctx.lineWidth = 2;
for (let x = 0; x < 400; x++) {
const y = 150 + Math.sin((x + time * (1 + ch*0.5)) * 0.03 + ch * 2) * (60 + ch * 20)
+ Math.sin((x + time) * 0.01) * 30;
if (x === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
if (mode === 'wave') {
animId = requestAnimationFrame(drawWaveform);
}
}
function drawClock() {
const cx = 200, cy = 150, r = 110;
const now = new Date();
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#444';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
// 刻度
for (let i = 0; i < 12; i++) {
const angle = (i * 30 - 90) * Math.PI / 180;
const inner = i % 3 === 0 ? r - 15 : r - 8;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(angle) * inner, cy + Math.sin(angle) * inner);
ctx.lineTo(cx + Math.cos(angle) * (r - 3), cy + Math.sin(angle) * (r - 3));
ctx.strokeStyle = i % 3 === 0 ? '#fff' : '#666';
ctx.lineWidth = i % 3 === 0 ? 3 : 1;
ctx.stroke();
}
// 时针
const hourAngle = ((now.getHours() % 12) * 30 + now.getMinutes() * 0.5 - 90) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(hourAngle) * 50, cy + Math.sin(hourAngle) * 50);
ctx.strokeStyle = '#fff';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.stroke();
// 分针
const minAngle = (now.getMinutes() * 6 + now.getSeconds() * 0.1 - 90) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(minAngle) * 75, cy + Math.sin(minAngle) * 75);
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 3;
ctx.stroke();
// 秒针
const secAngle = (now.getSeconds() * 6 - 90) * Math.PI / 180;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(secAngle) * 90, cy + Math.sin(secAngle) * 90);
ctx.strokeStyle = '#f55';
ctx.lineWidth = 1.5;
ctx.stroke();
// 中心点
ctx.beginPath();
ctx.arc(cx, cy, 5, 0, Math.PI * 2);
ctx.fillStyle = '#f55';
ctx.fill();
if (mode === 'clock') {
animId = requestAnimationFrame(drawClock);
}
}
// ====== 消息处理 ======
self.onmessage = function(e) {
const data = e.data;
if (data.type === 'init') {
ctx = data.canvas.getContext('2d');
self.postMessage({ type: 'ready' });
}
if (data.type === 'startParticles') {
if (animId) cancelAnimationFrame(animId);
mode = 'particles';
createParticles(data.count || 100);
lastTime = performance.now();
frameCount = 0;
drawParticles();
}
if (data.type === 'mandelbrot') {
if (animId) cancelAnimationFrame(animId);
mode = 'mandelbrot';
drawMandelbrot();
}
if (data.type === 'wave') {
if (animId) cancelAnimationFrame(animId);
mode = 'wave';
drawWaveform(performance.now());
}
if (data.type === 'clock') {
if (animId) cancelAnimationFrame(animId);
mode = 'clock';
drawClock();
}
if (data.type === 'stop') {
if (animId) cancelAnimationFrame(animId);
animId = null;
mode = 'idle';
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, 400, 300);
self.postMessage({ type: 'stopped' });
}
};
`;
// ====== 初始化 ======
function initWorker() {
if (!supportsOffscreenCanvas) {
log('⚠️ 浏览器不支持 OffscreenCanvas,将使用主线程 Canvas 回退模式', 'err');
return null;
}
try {
const canvas = document.getElementById('offscreenCanvas');
const offscreen = canvas.transferControlToOffscreen();
const blob = new Blob([renderWorkerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const d = e.data;
if (d.type === 'ready') {
log('Worker 已初始化 OffscreenCanvas', 'ok');
} else if (d.type === 'fps') {
document.getElementById('fpsValue').textContent = d.value;
} else if (d.type === 'done') {
log(`${d.mode} 渲染完成`, 'ok');
} else if (d.type === 'stopped') {
log('渲染已停止', 'info');
}
};
worker.onerror = function(e) {
log(`Worker 错误: ${e.message}`, 'err');
e.preventDefault();
};
// 发送 OffscreenCanvas 给 Worker
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
log('OffscreenCanvas 控制权已转移给 Worker', 'ok');
return worker;
} catch (err) {
log(`初始化失败: ${err.message}`, 'err');
return null;
}
}
// ====== 动画函数 ======
function startAnimation() {
stopAll();
updateStats('-', 100, '粒子动画', '运行中');
if (offscreenWorker && supportsOffscreenCanvas) {
offscreenWorker.postMessage({ type: 'startParticles', count: 100 });
log('▶ Worker 粒子动画启动 (100 个粒子)', 'ok');
} else {
// 回退到主线程
startMainParticleAnimation();
log('▶ 主线程回退模式: 粒子动画 (浏览器不支持 OffscreenCanvas)', 'warn');
}
}
// 主线程回退的粒子动画
function startMainParticleAnimation() {
const canvas = document.getElementById('mainCanvas');
const mctx = canvas.getContext('2d');
const particles = [];
for (let i = 0; i < 100; i++) {
particles.push({
x: Math.random() * 400, y: Math.random() * 300,
vx: (Math.random() - 0.5) * 4, vy: (Math.random() - 0.5) * 4,
r: Math.random() * 4 + 2, hue: Math.random() * 360
});
}
let frames = 0, lastT = performance.now(), fps = 0;
function animate() {
mctx.fillStyle = 'rgba(0,0,0,0.15)';
mctx.fillRect(0, 0, 400, 300);
particles.forEach(p => {
p.x += p.vx; p.y += p.vy; p.hue = (p.hue + 1) % 360;
if (p.x < 0 || p.x > 400) p.vx *= -1;
if (p.y < 0 || p.y > 300) p.vy *= -1;
mctx.beginPath();
mctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
mctx.fillStyle = `hsl(${p.hue},80%,60%)`;
mctx.fill();
});
frames++;
const now = performance.now();
if (now - lastT >= 1000) { fps = frames; frames = 0; lastT = now; document.getElementById('fpsValue').textContent = fps; }
mainAnimId = requestAnimationFrame(animate);
}
animate();
}
function drawMandelbrot() {
stopAll();
updateStats('-', '-', 'Mandelbrot', '运行中');
if (offscreenWorker && supportsOffscreenCanvas) {
offscreenWorker.postMessage({ type: 'mandelbrot' });
log('🌀 Worker 开始渲染 Mandelbrot 分形...', 'ok');
} else {
log('⚠️ 当前浏览器不支持 OffscreenCanvas,无法演示此功能', 'err');
}
}
function drawWaveform() {
stopAll();
updateStats('-', '-', '波形动画', '运行中');
if (offscreenWorker && supportsOffscreenCanvas) {
offscreenWorker.postMessage({ type: 'wave' });
log('〰️ Worker 波形动画启动', 'ok');
} else {
log('⚠️ 当前浏览器不支持 OffscreenCanvas', 'err');
}
}
function drawClock() {
stopAll();
updateStats('-', '-', '实时时钟', '运行中');
if (offscreenWorker && supportsOffscreenCanvas) {
offscreenWorker.postMessage({ type: 'clock' });
log('🕐 Worker 时钟启动', 'ok');
} else {
log('⚠️ 当前浏览器不支持 OffscreenCanvas', 'err');
}
}
function stopAll() {
if (mainAnimId) { cancelAnimationFrame(mainAnimId); mainAnimId = null; }
if (offscreenWorker && supportsOffscreenCanvas) {
offscreenWorker.postMessage({ type: 'stop' });
}
// 清空主线程 canvas
const mc = document.getElementById('mainCanvas').getContext('2d');
mc.fillStyle = '#000'; mc.fillRect(0, 0, 400, 300);
updateStats('-', 0, '空闲', '就绪');
log('⛹ 所有渲染已停止', 'info');
}
// 页面加载时初始化 Worker
window.addEventListener('load', function() {
offscreenWorker = initWorker();
});
</script>
</body>
</html>核心概念
传统 Canvas 渲染的痛点在于所有绘制操作(fillRect、drawImage、getImageData 等)都在主线程执行,当画布尺寸较大或绘制内容复杂时,会导致界面卡顿。OffscreenCanvas 将渲染工作转移到 Worker 中:
传统模式:
主线程: UI 交互 + Canvas 绘制 → 😫 卡顿
OffscreenCanvas 模式:
主线程: UI 交互(流畅)😊
Worker: Canvas 绘制(独立线程)→ 位图回传 → 主线程合成显示transferControlToOffscreen() 基础用法
<!-- index.html -->
<canvas id="mainCanvas" width="800" height="600"></canvas>// main.js - 主线程
const canvas = document.getElementById('mainCanvas')
// 1. 将控制权转移到 OffscreenCanvas
const offscreen = canvas.transferControlToOffscreen()
// 2. 将 OffscreenCanvas 传给 Worker
const worker = new Worker('render-worker.js', { type: 'module' })
worker.postMessage({ canvas: offscreen }, [offscreen])
// 3. 之后主线程不再操作此 canvas,所有绘制由 Worker 完成// render-worker.js - Worker 线程
self.onmessage = (e) => {
const { canvas } = e.data
const ctx = canvas.getContext('2d')
// 现在可以在 Worker 中自由绘制了!
function render(timestamp) {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 绘制内容
ctx.fillStyle = '#3498db'
ctx.fillRect(100 + Math.sin(timestamp / 500) * 50, 100, 200, 200)
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 3
ctx.beginPath()
ctx.arc(400, 300, 80 + Math.cos(timestamp / 300) * 20, 0, Math.PI * 2)
ctx.stroke()
// 请求下一帧
requestAnimationFrame(render)
}
// 启动渲染循环
requestAnimationFrame(render)
}视频处理实战:Worker 化视频帧处理
以下示例展示如何在 Worker 中逐帧处理视频数据,实现实时的视频滤镜效果而不影响主线程 UI:
图像滤镜 Worker 化实战
下面是一个更实用的完整示例——纯在 Worker 中进行图像处理的管线:
// image-pipeline-main.js - 主线程
const canvas = document.getElementById('canvas')
const offscreen = canvas.transferControlToOffscreen()
const pipelineWorker = new Worker('image-pipeline-worker.js', { type: 'module' })
pipelineWorker.postMessage({ canvas: offscreen }, [offscreen])
// 加载图片后发送给 Worker 处理
function loadImageAndProcess(imageUrl) {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
// 创建 ImageBitmap(可 transfer 到 Worker)
createImageBitmap(img).then((bitmap) => {
pipelineWorker.postMessage(
{
command: 'process',
image: bitmap,
pipeline: ['brightness', 'contrast', 'sharpen'],
params: { brightness: 1.2, contrast: 1.1 }
},
[bitmap]
)
})
}
img.src = imageUrl
}// image-pipeline-worker.js - Worker 线程
self.onmessage = async (e) => {
const { command, canvas, image, pipeline, params } = e.data
if (!canvas) return
const ctx = canvas.getContext('2d')
const offscreenCanvas = new OffscreenCanvas(canvas.width, canvas.height)
const offCtx = offscreenCanvas.getContext('2d')
// 绘制原始图片到离屏 Canvas
offCtx.drawImage(image, 0, 0)
let imageData = offCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height)
// 按流水线依次应用滤镜
for (const step of pipeline) {
imageData = applyFilter(step, imageData, params)
}
// 将结果绘制到主 canvas(通过 OffscreenCanvas 自动同步)
ctx.putImageData(imageData, 0, 0)
self.postMessage({ status: 'done', pipeline })
}
function applyFilter(filterName, imageData, params) {
const data = imageData.data
const width = imageData.width
const height = imageData.height
switch (filterName) {
case 'brightness':
const bFactor = params.brightness || 1
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.min(255, data[i] * bFactor)
data[i + 1] = Math.min(255, data[i + 1] * bFactor)
data[i + 2] = Math.min(255, data[i + 2] * bFactor)
}
break
case 'contrast':
const cFactor = params.contrast || 1
const intercept = 128 * (1 - cFactor)
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.max(0, Math.min(255, data[i] * cFactor + intercept))
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] * cFactor + intercept))
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] * cFactor + intercept))
}
break
case 'sharpen':
// 卷积核锐化
const kernel = [0, -1, 0, -1, 5, -1, 0, -1, 0]
applyConvolution(data, width, height, kernel)
break
}
return imageData
}
function applyConvolution(data, width, height, kernel) {
const copy = new Uint8ClampedArray(data)
const kSize = 3
const half = Math.floor(kSize / 2)
for (let y = half; y < height - half; y++) {
for (let x = half; x < width - half; x++) {
let r = 0, g = 0, b = 0
for (let ky = 0; ky < kSize; ky++) {
for (let kx = 0; kx < kSize; kx++) {
const px = x + kx - half
const py = y + ky - half
const idx = (py * width + px) * 4
const ki = ky * kSize + kx
r += copy[idx] * kernel[ki]
g += copy[idx + 1] * kernel[ki]
b += copy[idx + 2] * kernel[ki]
}
}
const idx = (y * width + x) * 4
data[idx] = Math.max(0, Math.min(255, r))
data[idx + 1] = Math.max(0, Math.min(255, g))
data[idx + 2] = Math.max(0, Math.min(255, b))
}
}
}OffscreenCanvas 兼容性与注意事项
| 浏览器 | OffscreenCanvas | transferControlToOffscreen |
|---|---|---|
| Chrome 69+ | ✅ | ✅ |
| Firefox 105+ | ✅ | ✅ |
| Safari 16.4+ | ✅ | ✅ |
| Edge 79+ | ✅ | ✅ |
transferControlToOffscreen()只能调用一次:一旦转移控制权,主线程不能再对该<canvas>元素调用getContext()等方法- 部分 Canvas API 在 OffscreenCanvas 中不可用:如
isPointInPath()、measureText()等涉及布局计算的 API toDataURL()/toBlob()不可用:如需导出图片,应在 Worker 中使用convertToBlob()替代- WebGL 上下文同样支持:
offscreen.getContext('webgl2')可以在 Worker 中使用 WebGL 渲染
最佳实践总结
<h4>004-worker-error-handling.html</h4><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【4】Worker 错误处理</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: onerror / onmessageerror 捕获和处理 Worker 错误
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #fafafa; color: #333;
}
.container { max-width: 850px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #eb3349, #f45c43);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: white; border-radius: 10px; padding: 24px; margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.card-title {
font-size: 16px; font-weight: 600; color: #333;
border-left: 4px solid #eb3349; padding-left: 12px; margin-bottom: 16px;
}
.info-box {
background: #fff3cd; border-left: 4px solid #ffc107;
padding: 12px 16px; border-radius: 4px; font-size: 13px; line-height: 1.6;
color: #856404; margin-bottom: 16px;
}
.btn-grid { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
.btn {
padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn:hover { transform: translateY(-1px); }
.btn-syntax { background: #eb3349; color: white; }
.btn-runtime { background: #f45c43; color: white; }
.btn-type { background: #e74c3c; color: white; }
.btn-resource { background: #c0392b; color: white; }
.btn-normal { background: #27ae60; color: white; }
.console-panel {
background: #1e1e1e; border-radius: 8px; padding: 16px;
font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 12px;
min-height: 300px; max-height: 450px; overflow-y: auto; color: #d4d4d4;
}
.log-entry { padding: 4px 0; border-bottom: 1px solid #2d2d2d; display: flex; gap: 10px; }
.log-time { color: #6a9955; min-width: 80px; }
.log-msg { flex: 1; }
.log-entry.error { color: #f44747; }
.log-entry.error .log-msg::before { content: '✗ '; }
.log-entry.warn { color: #cca700; }
.log-entry.success { color: #4ec9b0; }
.log-entry.success .log-msg::before { content: '✓ '; }
.log-entry.info { color: #569cd6; }
.error-detail {
background: #2d2d2d; border-radius: 6px; padding: 12px; margin-top: 8px;
font-size: 11px; color: #888; display: none;
}
.error-detail.show { display: block; }
table {
width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 12px;
}
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f5f5f5; font-weight: 600; color: #555; }
tr:hover td { background: #fafafa; }
.compat-badge {
display: inline-block; padding: 3px 10px; border-radius: 12px;
font-size: 11px; font-weight: 600;
}
.badge-good { background: #d4edda; color: #155724; }
.badge-warn { background: #fff3cd; color: #856404; }
.compat-table { margin-top: 16px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🛡️ Worker 错误处理机制</h1>
<p>onerror / onmessageerror 演示 — 捕获和处理各类 Worker 错误</p>
</div>
<div class="card">
<div class="card-title">错误触发按钮</div>
<div class="info-box">
⚡ 点击下方按钮模拟不同类型的 Worker 错误,观察控制台输出。<br>
<strong>onerror</strong> 捕获 Worker 脚本中的运行时错误;<strong>onmessageerror</strong> 捕获无法反序列化的消息数据错误。
</div>
<div class="btn-grid">
<button class="btn btn-syntax" onclick="triggerError('syntax')">📝 语法错误 (SyntaxError)</button>
<button class="btn btn-runtime" onclick="triggerError('runtime')">⚠️ 运行时错误 (ReferenceError)</button>
<button class="btn btn-type" onclick="triggerError('type')">🔄 类型错误 (TypeError)</button>
<button class="btn btn-resource" onclick="triggerError('resource')">🔗 资源加载错误 (404)</button>
<button class="btn btn-normal" onclick="triggerError('normal')">✅ 正常消息传递</button>
<button class="btn btn-normal" onclick="clearConsole()">🧹 清空控制台</button>
</div>
<div class="console-panel" id="console">
<div class="log-entry info">
<span class="log-time">[--:--:--]</span>
<span class="log-msg">控制台已就绪,点击上方按钮触发各类错误...</span>
</div>
</div>
</div>
<div class="card">
<div class="card-title">错误事件详解</div>
<table>
<thead>
<tr>
<th>事件</th>
<th>触发场景</th>
<th>ErrorEvent 属性</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>onerror</code></td>
<td>Worker 脚本语法错误、运行时异常</td>
<td><code>message, filename, lineno, colno, error</code></td>
<td>捕获 Worker 内部抛出的异常</td>
</tr>
<tr>
<td><code>onmessageerror</code></td>
<td>接收无法反序列化的数据(如 DOM 对象、函数)</td>
<td><code>message, filename, lineno, colno, error</code></td>
<td>结构化克隆失败时触发</td>
</tr>
</tbody>
</table>
<table class="compat-table">
<thead>
<tr><th colspan="2">浏览器兼容性</th></tr>
</thead>
<tbody>
<tr>
<td><code>worker.onerror</code></td>
<td><span class="compat-badge badge-good">Chrome 4+</span> <span class="compat-badge badge-good">Firefox 3.5+</span> <span class="compat-badge badge-good">Safari 4+</span></td>
</tr>
<tr>
<td><code>worker.onmessageerror</code></td>
<td><span class="compat-badge badge-good">Chrome 60+</span> <span class="compat-badge badge-good">Firefox 57+</span> <span class="compat-badge badge-warn">Safari 不支持</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
const consoleEl = document.getElementById('console');
function log(msg, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
const now = new Date();
entry.innerHTML = `<span class="log-time">${now.toTimeString().substring(0,8)}</span><span class="log-msg">${msg}</span>`;
consoleEl.appendChild(entry);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function clearConsole() {
consoleEl.innerHTML = '';
log('控制台已清空', 'info');
}
function triggerError(type) {
log(`--- 触发 ${type} 类型错误 ---`, 'warn');
switch (type) {
case 'syntax': {
// 语法错误的代码
const badCode = `
function broken( {
console.log("missing closing paren"
}
self.onmessage = function(e) {};
`;
const blob = new Blob([badCode], { type: 'application/javascript' });
try {
const w = new Worker(URL.createObjectURL(blob));
w.onerror = function(e) {
log(`[onerror] SyntaxError: ${e.message}`, 'error');
log(` 文件: ${e.filename} | 行: ${e.lineno} | 列: ${e.colno}`, 'error');
e.preventDefault(); // 阻止错误继续传播
w.terminate();
};
w.onmessageerror = function(e) {
log(`[onmessageerror] ${e.message || '反序列化失败'}`, 'error');
};
} catch (err) {
log(`[catch] 构造异常: ${err.message}`, 'error');
}
break;
}
case 'runtime': {
const runtimeCode = `
self.onmessage = function(e) {
// 故意引用未定义变量
const result = undefinedVariable.doSomething();
self.postMessage(result);
};
`;
const blob = new Blob([runtimeCode], { type: 'application/javascript' });
const w = new Worker(URL.createObjectURL(blob));
w.onerror = function(e) {
log(`[onerror] ReferenceError: ${e.message}`, 'error');
log(` 文件: ${e.filename} | 行号: ${e.lineno}`, 'error');
e.preventDefault();
w.terminate();
};
w.onmessageerror = function(e) {
log(`[onmessageerror] ${e.message || '反序列化失败'}`, 'error');
};
w.postMessage('trigger');
break;
}
case 'type': {
const typeCode = `
self.onmessage = function(e) {
const num = e.data;
// 将非函数当作函数调用
const result = num();
self.postMessage(result);
};
`;
const blob = new Blob([typeCode], { type: 'application/javascript' });
const w = new Worker(URL.createObjectURL(blob));
w.onerror = function(e) {
log(`[onerror] TypeError: ${e.message}`, 'error');
log(` 文件: ${e.filename} | 行号: ${e.lineno}`, 'error');
e.preventDefault();
w.terminate();
};
w.onmessageerror = function(e) {
log(`[onmessageerror] ${e.message || '反序列化失败'}`, 'error');
};
w.postMessage(42);
break;
}
case 'resource': {
// 尝试加载不存在的脚本
try {
const w = new Worker('./non-existent-worker-file-xyz.js');
w.onerror = function(e) {
log(`[onerror] 资源加载失败: ${e.message}`, 'error');
log(` 这通常意味着 Worker 脚本文件不存在或路径错误`, 'error');
e.preventDefault();
w.terminate();
};
w.onmessageerror = function(e) {
log(`[onmessageerror] ${e.message || '反序列化失败'}`, 'error');
};
} catch (err) {
log(`[catch] ${err.name}: ${err.message}`, 'error');
}
break;
}
case 'normal': {
const normalCode = `
self.onmessage = function(e) {
self.postMessage({ status: 'ok', echo: e.data, timestamp: Date.now() });
};
`;
const blob = new Blob([normalCode], { type: 'application/javascript' });
const w = new Worker(URL.createObjectURL(blob));
w.onmessage = function(e) {
log(`[onmessage] 正常接收: ${JSON.stringify(e.data)}`, 'success');
w.terminate();
};
w.onerror = function(e) {
log(`[onerror] ${e.message}`, 'error');
e.preventDefault();
w.terminate();
};
w.onmessageerror = function(e) {
log(`[onmessageerror] 反序列化错误`, 'error');
};
w.postMessage({ test: 'hello from main thread' });
// 同时测试不可序列化的数据
log('', 'info');
log('尝试发送不可序列化数据 (DOM 元素)...', 'warn');
try {
w.postMessage(document.body); // DOM 对象无法克隆
} catch (err) {
log(`[DataCloneError] 无法克隆: ${err.name}: ${err.message.substring(0, 60)}`, 'error');
}
break;
}
}
}
</script>
</body>
</html>1. 何时使用 Web Workers
适合使用 Worker 的场景:
- 处理大量数据(数组、字符串、图像等)
- 执行复杂计算(加密、压缩、图像处理)
- 需要保持 UI 响应性的长时间运行任务
- 需要并行处理多个独立任务
不适合使用 Worker 的场景:
- 简单的 DOM 操作
- 轻量级计算(开销可能大于收益)
- 需要频繁访问 DOM 的任务
- 需要访问
localStorage或sessionStorage的任务
2. 代码组织建议
// ✅ 好的实践:使用类封装 Worker 管理
class TaskProcessor {
constructor() {
this.worker = new Worker("worker.js")
this.setupHandlers()
}
setupHandlers() {
this.worker.onmessage = this.handleMessage.bind(this)
this.worker.onerror = this.handleError.bind(this)
}
process(data) {
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
this.worker.postMessage(data)
})
}
handleMessage(event) {
if (this.resolve) {
this.resolve(event.data)
}
}
handleError(error) {
if (this.reject) {
this.reject(error)
}
}
destroy() {
this.worker.terminate()
}
}3. 错误处理模式
// 统一的错误处理模式
class SafeWorker {
constructor(script) {
this.worker = new Worker(script)
this.setupErrorHandling()
}
setupErrorHandling() {
this.worker.addEventListener("error", (error) => {
console.error("Worker 错误:", {
message: error.message,
filename: error.filename,
lineno: error.lineno
})
// 发送错误到监控系统
this.reportError(error)
})
this.worker.addEventListener("messageerror", (error) => {
console.error("消息传递错误:", error)
})
}
postMessage(data) {
try {
this.worker.postMessage(data)
} catch (error) {
console.error("发送消息失败:", error)
}
}
reportError(error) {
// 实现错误上报逻辑
}
}4. 内存管理
- 及时终止不再使用的 Worker
- 对于大型数据,使用 Transferable Objects
- 避免在 Worker 中创建大量全局变量
- 定期清理不需要的数据引用
常见问题(FAQ)
Q1: Worker 可以访问哪些全局对象?
Worker 可以访问:
self(Worker 的全局对象)navigator(精简版)consoleJSON、Math、Date等标准对象fetch、XMLHttpRequest(网络请求)setTimeout、setInterval(定时器)
Worker 不能访问:
window、document(DOM 相关)parent、top、frameslocalStorage、sessionStorage
Q2: 如何检测浏览器是否支持 Web Workers?
if (typeof Worker !== "undefined") {
// 支持 Web Workers
const worker = new Worker("worker.js")
} else {
// 不支持,使用降级方案
console.warn("浏览器不支持 Web Workers")
}Q3: Worker 可以共享变量吗?
不可以。Worker 和主线程有完全独立的作用域,只能通过 postMessage 传递数据。如果需要共享状态,可以使用:
- Shared Worker(跨页面共享)
- IndexedDB(持久化存储)
- 主线程作为中介进行状态同步
Q4: 如何传递函数给 Worker?
不能直接传递函数。如果需要传递逻辑,可以:
- 传递函数字符串,在 Worker 中使用
eval()(不推荐) - 传递配置对象,Worker 根据配置执行不同逻辑
- 使用
importScripts导入包含函数的脚本
// ❌ 不能这样做
worker.postMessage({ fn: function () {} })
// ✅ 可以这样做
worker.postMessage({
action: "calculate",
params: [1, 2, 3]
})Q5: Worker 中的 this 指向什么?
在 Worker 中,this 指向 self(Worker 的全局对象)。建议使用 self 而不是 this,代码更清晰。
// 在 Worker 中
console.log(this === self) // true
self.onmessage = function (event) {
// 使用 self 更明确
}Q6: 如何实现 Worker 之间的通信?
使用 MessageChannel 可以在两个 Worker 之间建立直接通信通道,详见 MessageChannel 部分。
Q7: Worker 可以访问 Cookie 吗?
Worker 不能直接访问 Cookie,但可以通过以下方式:
- 在主线程中读取 Cookie,然后通过
postMessage传递给 Worker - 在 Worker 中使用
fetch请求时,Cookie 会自动包含在请求头中(如果设置了credentials: 'include')
Q8: 如何调试 Worker 代码?
- 在 Chrome DevTools 的 Sources 面板中找到 Worker 脚本
- 设置断点进行调试
- 使用
console.log输出调试信息 - 在 Console 面板中可以看到 Worker 的输出
详见 Web Worker 调试与性能分析 章节。
Q9: Worker 会影响页面性能吗?
Worker 在独立线程中运行,不会阻塞主线程,但会占用系统资源:
- 每个 Worker 会消耗内存
- 过多的 Worker 可能导致 CPU 过载
- 建议 Worker 数量不超过 CPU 核心数
Q10: 如何处理 Worker 加载失败?
let worker
try {
worker = new Worker("worker.js")
} catch (error) {
console.error("Worker 创建失败:", error)
// 使用降级方案
fallbackToMainThread()
}
worker.addEventListener("error", (error) => {
if (error.filename && error.lineno === 0) {
// 脚本加载失败
console.error("Worker 脚本加载失败")
}
})Q11: Worker 中无法访问 DOM,有哪些替代方案?
这是 Worker 最常见的限制之一,以下是成熟的替代方案:
方案一:主线程代理模式(最常用)
// Worker 中需要 DOM 操作时,发送指令给主线程执行
// worker.js
self.onmessage = (e) => {
const { action, selector, text } = e.data
// 不直接操作 DOM,而是通知主线程
self.postMessage({
type: 'dom-update',
action: 'setTextContent',
selector: '#result',
text: `计算结果: ${text}`
})
}
// main.js
worker.onmessage = (e) => {
if (e.data.type === 'dom-update') {
const el = document.querySelector(e.data.selector)
if (el && e.data.action === 'setTextContent') {
el.textContent = e.data.text
}
}
}方案二:HTML Template 预渲染
// 在主线程预先准备模板和数据
// Worker 只负责计算数据,主线程负责渲染
const template = document.getElementById('row-template')
worker.onmessage = (e) => {
const rows = e.data.rows
const container = document.getElementById('table-body')
container.innerHTML = ''
rows.forEach(row => {
const clone = template.content.cloneNode(true)
clone.querySelector('.name').textContent = row.name
clone.querySelector('.value').textContent = row.value
container.appendChild(clone)
})
}方案三:Virtual DOM / 库辅助
// 使用轻量级虚拟 DOM 库(如 uhtml、lighterhtml 等)
// 这些库支持在 Worker 中构建 DOM 树,然后一次性同步到主线程方案四:CSS-in-JS + 数据驱动
// Worker 计算样式数据和布局信息
// 主线程根据数据应用样式
worker.onmessage = (e) => {
const { elementId, styles } = e.data
const el = document.getElementById(elementId)
Object.assign(el.style, styles)
}| 方案 | 适用场景 | 复杂度 | 推荐度 |
|---|---|---|---|
| 主线程代理 | 通用场景 | 低 | ⭐⭐⭐⭐⭐ |
| HTML Template | 列表/表格渲染 | 低 | ⭐⭐⭐⭐ |
| Virtual DOM | 复杂 UI 构建 | 中 | ⭐⭐⭐ |
| 数据驱动样式 | 动画/可视化 | 低 | ⭐⭐⭐⭐ |
Q12: Worker 脚本加载遇到 CORS 问题怎么办?
Worker 脚本受同源策略约束,跨域加载时会遇到 CORS 问题。
问题诊断
const worker = new Worker('https://cdn.example.com/worker.js')
worker.onerror = (error) => {
// CORS 错误的特征:
// - error.message 通常为 "Script error." 或空
// - error.filename 为空字符串 ""
// - error.lineno 为 0
if (!error.filename && error.lineno === 0) {
console.error('❌ 这是 CORS 错误!Worker 脚本跨域加载失败')
}
}解决方案
方案 A:服务器配置 CORS 响应头(推荐)
# Nginx 配置
location /workers/ {
add_header Access-Control-Allow-Origin * always;
# 或者指定具体域名
# add_header Access-Control-Allow-Origin https://your-app.com always;
}// Node.js / Express
app.get('/workers/:filename', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', '*')
res.setHeader('Content-Type', 'application/javascript')
// 返回 Worker 脚本内容
})方案 B:将 Worker 脚本放到同源服务器
// 开发环境: Vite/Webpack 会自动处理
import MyWorker from './worker?worker'
const worker = new MyWorker()
// 生产环境: 将 worker.js 打包到同域名下
const worker = new Worker('/assets/worker-[hash].js')方案 C:使用 Blob Worker 绕过(临时方案)
// 从 CDN 获取脚本内容,转为 Blob URL
fetch('https://cdn.example.com/worker.js')
.then(r => r.text())
.then(code => {
const blob = new Blob([code], { type: 'application/javascript' })
const worker = new Worker(URL.createObjectURL(blob))
})方案 C 存在安全隐患(等同于 eval 远程代码),仅建议在可信 CDN 场景下临时使用。生产环境应优先采用方案 A 或 B。
Q13: 浏览器对 Worker 数量有限制吗?
浏览器没有明确规定 Worker 的最大数量,但存在实际的资源限制:
实际限制因素
| 因素 | 限制 | 说明 |
|---|---|---|
| 内存 | 每个 Worker 约 1MB+ 基础开销 | 内存不足时创建会失败 |
| CPU 核心 | navigator.hardwareConcurrency | 超过核心数的 Worker 会导致频繁上下文切换 |
| 浏览器实现 | Chrome 约数百个上限 | 不同浏览器行为不同 |
| 端口数 | 每个 Worker 占用一个端口 | 系统端口总数有限(~65535) |
最佳实践
// ✅ 推荐: 根据 CPU 核心数决定 Worker 数量
const OPTIMAL_WORKER_COUNT = Math.min(
navigator.hardwareConcurrency || 4, // 不超过 CPU 核心数
8 // 设置硬上限防止过多
)
console.log(`推荐 Worker 数量: ${OPTIMAL_WORKER_COUNT}`)
// ❌ 避免: 无限制创建 Worker
function badPattern(dataList) {
dataList.forEach(item => {
const worker = new Worker('worker.js') // 可能创建数千个!
worker.postMessage(item)
})
}
// ✅ 推荐: 使用 Worker 池
function goodPattern(dataList) {
const pool = new WorkerPool('worker.js', OPTIMAL_WORKER_COUNT)
return Promise.all(dataList.map(item => pool.execute(item)))
}监控 Worker 资源占用
// 使用 Performance API 监控 Worker 创建的开销
function monitorWorkerCreation() {
const start = performance.now()
const worker = new Worker('worker.js')
const duration = performance.now() - start
console.log(`Worker 创建耗时: ${duration.toFixed(2)}ms`)
// 估算内存(粗略)
if (performance.memory) {
console.log(`当前 JS 堆内存: ${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1)}MB`)
}
}总结
Web Workers 是提升 Web 应用性能的重要工具,通过将计算密集型任务移到后台线程,可以显著改善用户体验。关键要点:
- 选择合适的场景:不是所有任务都需要 Worker
- 合理管理生命周期:及时创建和销毁 Worker
- 优化数据传输:使用 Transferable Objects 减少复制开销
- 完善的错误处理:确保 Worker 错误不会影响主应用
- 性能监控:跟踪 Worker 的执行时间和资源使用
- 善用构建工具:Vite/Webpack 提供了便捷的 Worker 集成方案
- 利用 OffscreenCanvas:将渲染任务完全移入 Worker 线程
- 重视调试能力:熟练运用 DevTools 的 Threads/Memory 面板
通过合理使用 Web Workers,可以构建更流畅、响应更快的 Web 应用。
补充示例
<h4>001-worker-calculation.html</h4><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【1】Web Worker 计算演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 20px; background: #f8f9fa; color: #333; }
.demo-container { max-width: 700px; margin: 0 auto; background: white; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.demo-title { margin-bottom: 16px; font-size: 18px; color: #555; border-bottom: 2px solid #007bff; padding-bottom: 8px; }
.info-box {
background: #e7f3ff; border-left: 4px solid #007bff;
padding: 14px 16px; margin-bottom: 20px; border-radius: 4px;
font-size: 14px; line-height: 1.6;
}
.input-group { display: flex; gap: 12px; align-items: center; margin-bottom: 20px; flex-wrap: wrap; }
label { font-weight: 500; color: #444; }
input[type="number"] {
padding: 10px 14px; border: 2px solid #e0e0e0; border-radius: 6px;
width: 140px; font-size: 15px; transition: border-color 0.3s;
}
input:focus { border-color: #007bff; outline: none; }
.btn {
padding: 10px 24px; border: none; border-radius: 6px;
cursor: pointer; font-size: 14px; font-weight: 500;
transition: all 0.3s;
}
.btn-primary { background: #007bff; color: white; }
.btn-primary:hover { background: #0056b3; transform: translateY(-1px); }
.btn-primary:disabled { background: #ccc; cursor: not-allowed; transform: none; }
.result-panel {
background: #f8f9fa; border-radius: 8px; padding: 20px; margin-top: 20px;
border: 1px solid #e0e0e0;
}
.result-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #eee; font-size: 15px; }
.result-row:last-child { border-bottom: none; }
.result-label { color: #666; }
.result-value { font-weight: 600; color: #333; font-family: monospace; font-size: 17px; }
.result-value.success { color: #28a745; }
.progress-bar-container {
height: 8px; background: #e9ecef; border-radius: 4px;
overflow: hidden; margin-top: 16px; display: none;
}
.progress-bar-fill {
height: 100%; background: linear-gradient(90deg, #007bff, #28a745);
border-radius: 4px; transition: width 0.2s ease; width: 0%;
}
.log-area {
margin-top: 16px; background: #1e1e1e; color: #d4d4d4; border-radius: 6px;
padding: 14px; font-family: monospace; font-size: 13px; max-height: 200px; overflow-y: auto;
}
.log-entry { margin-bottom: 4px; }
.log-entry.info { color: #569cd6; }
.log-entry.success { color: #4ec9b0; }
.log-entry.warn { color: #dcdcaa; }
</style>
</head>
<body>
<div class="demo-container">
<div class="demo-title">示例:Web Worker 后台计算(Blob URL 内联)</div>
<div class="info-box">
💡 <strong>原理:</strong>使用 Blob URL 将 Worker 代码内联到单个 HTML 文件中。
Worker 在独立线程执行计算任务,不阻塞主线程 UI。
</div>
<div class="input-group">
<label for="numInput">计算 1 到 N 的累加:</label>
<input type="number" id="numInput" value="100000000" min="1" step="1000000" />
<button class="btn btn-primary" id="calcBtn" onclick="startCalculation()">🚀 开始计算</button>
</div>
<div class="result-panel">
<div class="result-row">
<span class="result-label">输入值 (N)</span>
<span class="result-value" id="inputValue">-</span>
</div>
<div class="result-row">
<span class="result-label">计算结果</span>
<span class="result-value success" id="calcResult">等待计算...</span>
</div>
<div class="result-row">
<span class="result-label">计算耗时</span>
<span class="result-value" id="calcTime">-</span>
</div>
<div class="result-row">
<span class="result-label">Worker 状态</span>
<span class="result-value" id="workerStatus">就绪</span>
</div>
</div>
<div class="progress-bar-container" id="progressContainer">
<div class="progress-bar-fill" id="progressBar"></div>
</div>
<div class="log-area" id="logArea">
<div class="log-entry info">[系统] Web Worker 已准备就绪</div>
</div>
</div>
<script>
const logArea = document.getElementById("logArea")
function log(msg, type = "info") {
const entry = document.createElement("div")
entry.className = `log-entry ${type}`
entry.textContent = msg
logArea.appendChild(entry)
logArea.scrollTop = logArea.scrollHeight
}
// ====== 使用 Blob URL 创建内联 Worker ======
const workerCode = `
self.onmessage = function(event) {
const n = event.data
self.postMessage({ type: 'start', n: n })
// 模拟耗时计算:计算 1 到 N 的累加
let sum = 0
const startTime = performance.now()
for (let i = 1; i <= n; i++) {
sum += i
// 每 5% 发送一次进度
if (i % Math.floor(n / 20) === 0) {
self.postMessage({ type: 'progress', percent: Math.round((i / n) * 100), current: i })
}
}
const elapsed = performance.now() - startTime
self.postMessage({
type: 'done',
result: sum,
time: elapsed.toFixed(2),
n: n
})
self.close()
}
self.onerror = function(error) {
self.postMessage({ type: 'error', message: error.message })
}
`
const blob = new Blob([workerCode], { type: "application/javascript" })
const workerURL = URL.createObjectURL(blob)
let worker = null
document.getElementById("workerStatus").textContent = "已创建 (Blob URL)"
function startCalculation() {
const num = parseInt(document.getElementById("numInput").value)
if (!num || num < 1) {
alert("请输入有效的正整数")
return
}
// 更新UI状态
document.getElementById("inputValue").textContent = num.toLocaleString()
document.getElementById("calcResult").textContent = "计算中..."
document.getElementById("calcTime").textContent = "-"
document.getElementById("workerStatus").textContent = "运行中..."
document.getElementById("calcBtn").disabled = true
document.getElementById("progressContainer").style.display = "block"
document.getElementById("progressBar").style.width = "0%"
log(`[开始] 计算 1 到 ${num.toLocaleString()} 的累加`, "info")
// 创建新的 Worker 实例
worker = new Worker(workerURL)
worker.onmessage = function(event) {
const data = event.data
switch (data.type) {
case "start":
log(`[Worker] 开始计算,N = ${data.n.toLocaleString()}`, "info")
break
case "progress":
document.getElementById("progressBar").style.width = `${data.percent}%`
document.getElementById("calcResult").textContent = `计算中... ${data.percent}%`
break
case "done":
document.getElementById("calcResult").textContent = data.result.toLocaleString()
document.getElementById("calcTime").textContent = `${data.time} ms`
document.getElementById("workerStatus").textContent = "已完成 ✓"
document.getElementById("calcBtn").disabled = false
log(`[完成] 结果: ${data.result.toLocaleString()} | 耗时: ${data.time}ms`, "success")
break
case "error":
document.getElementById("calcResult").textContent = `错误: ${data.message}`
document.getElementById("workerStatus").textContent = "错误 ✗"
document.getElementById("calcBtn").disabled = false
log(`[错误] ${data.message}`, "warn")
break
}
}
worker.onerror = function(error) {
document.getElementById("workerStatus").textContent = "错误 ✗"
document.getElementById("calcBtn").disabled = false
log(`[Worker 错误] ${error.message} (${error.filename}:${error.lineno})`, "warn")
}
// 发送计算任务
worker.postMessage(num)
}
</script>
</body>
</html>```
<h4>003-worker-intensive-compute.html</h4>
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【3】Worker 密集计算 - 斐波那契/素数</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Worker 中执行密集计算,主线程不阻塞演示
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #1a1a2e; color: #eee;
min-height: 100vh;
}
.container { max-width: 1000px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; margin-bottom: 24px;
}
.header h1 { font-size: 24px; color: #00d9ff; margin-bottom: 8px; }
.header p { color: #888; font-size: 14px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
.panel {
background: #16213e; border-radius: 12px; padding: 20px;
border: 1px solid #0f3460;
}
.panel-title {
font-size: 15px; font-weight: 600; color: #00d9ff;
margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid #0f3460;
}
.control-row { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
label { font-size: 13px; color: #aaa; }
select, input[type="number"] {
padding: 8px 12px; background: #0f3460; border: 1px solid #1a4a7a;
color: #fff; border-radius: 6px; font-size: 13px;
}
select:focus, input:focus { outline: none; border-color: #00d9ff; }
.btn {
padding: 8px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-cyan { background: #00d9ff; color: #1a1a2e; }
.btn-cyan:hover { background: #00b8d9; }
.btn-green { background: #00ff88; color: #1a1a2e; }
.btn-green:hover { background: #00cc6a; }
.btn-red { background: #ff4757; color: white; }
.btn-red:hover { background: #ee3344; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.result-area {
background: #0a0a1a; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 13px; min-height: 120px;
max-height: 200px; overflow-y: auto; color: #ccc;
}
.result-line { margin-bottom: 4px; line-height: 1.5; }
.result-line.highlight { color: #00ff88; }
.result-line.warn { color: #ffa502; }
.result-line.error { color: #ff4757; }
.stats-row {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
margin-top: 12px;
}
.stat-box {
background: #0f3460; border-radius: 6px; padding: 10px; text-align: center;
}
.stat-value { font-size: 18px; font-weight: 700; color: #00d9ff; }
.stat-label { font-size: 11px; color: #888; margin-top: 2px; }
/* 动画指示器 */
.busy-indicator {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 14px; background: rgba(255,167,38,0.15);
border-radius: 20px; font-size: 12px; color: #ffa726; margin-top: 10px;
}
.spinner {
width: 14px; height: 14px; border: 2px solid transparent;
border-top-color: #ffa726; border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.ui-test-area {
background: #16213e; border-radius: 12px; padding: 20px;
border: 1px solid #0f3460; margin-top: 20px;
}
.animation-box {
width: 60px; height: 60px; background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 8px; animation: bounce 1s ease-in-out infinite;
display: inline-block;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.ui-test-text { margin-left: 20px; color: #aaa; font-size: 13px; vertical-align: middle; }
.compat-note {
background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.3);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ Worker 密集计算演示</h1>
<p>斐波那契数列 & 素数计算 — 验证主线程不阻塞</p>
</div>
<div class="grid">
<!-- 斐波那契计算 -->
<div class="panel">
<div class="panel-title">🔢 斐波那契数列计算</div>
<div class="control-row">
<label>第 N 项:</label>
<input type="number" id="fibN" value="45" min="1" max="55" />
<button class="btn btn-cyan" id="fibBtnMain" onclick="calcFibonacci(false)">主线程计算</button>
<button class="btn btn-green" id="fibBtnWorker" onclick="calcFibonacci(true)">Worker 计算</button>
</div>
<div class="result-area" id="fibResult">
<div class="result-line">等待输入参数...</div>
</div>
<div class="stats-row">
<div class="stat-box"><div class="stat-value" id="fibAns">-</div><div class="stat-label">结果</div></div>
<div class="stat-box"><div class="stat-value" id="fibTime">-</div><div class="stat-label">耗时(ms)</div></div>
<div class="stat-box"><div class="stat-value" id="fibMethod">-</div><div class="stat-label">方式</div></div>
</div>
<div class="busy-indicator" id="fibBusy" style="display:none;"><div class="spinner"></div>计算中...</div>
</div>
<!-- 素数计算 -->
<div class="panel">
<div class="panel-title">🔍 素数统计 (范围内)</div>
<div class="control-row">
<label>范围:</label>
<input type="number" id="primeMax" value="1000000" min="1000" step="100000" />
<button class="btn btn-cyan" id="primeBtnMain" onclick="calcPrimes(false)">主线程计算</button>
<button class="btn btn-green" id="primeBtnWorker" onclick="calcPrimes(true)">Worker 计算</button>
</div>
<div class="result-area" id="primeResult">
<div class="result-line">等待输入参数...</div>
</div>
<div class="stats-row">
<div class="stat-box"><div class="stat-value" id="primeCount">-</div><div class="stat-label">素数个数</div></div>
<div class="stat-box"><div class="stat-value" id="primeTime">-</div><div class="stat-label">耗时(ms)</div></div>
<div class="stat-box"><div class="stat-value" id="primeMethod">-</div><div class="stat-label">方式</div></div>
</div>
<div class="busy-indicator" id="primeBusy" style="display:none;"><div class="spinner"></div>计算中...</div>
</div>
</div>
<!-- 主线程 UI 测试区域 -->
<div class="ui-test-area">
<div class="panel-title" style="border:none; margin-bottom:12px;">🎮 主线程响应测试</div>
<p style="color:#888; font-size:13px; margin-bottom:12px;">
上方使用<strong style="color:#ff4757;">主线程计算</strong>时,下方动画会卡顿;使用 <strong style="color:#00ff88;">Worker 计算</strong> 时动画流畅。
</p>
<div style="display:flex; align-items:center;">
<div class="animation-box"></div>
<span class="ui-test-text">✨ 如果这个方块动画流畅 → 主线程未被阻塞</span>
</div>
<div style="margin-top: 16px;">
<input type="text" placeholder="尝试在此输入文字,验证主线程是否响应..."
style="width:100%; padding:10px; background:#0a0a1a; border:1px solid #0f3460; border-radius:6px; color:#fff; font-size:13px;" />
</div>
</div>
<div class="compat-note">
⚠️ <strong>兼容性提示:</strong>Web Workers 在所有现代浏览器均支持。IE 不支持。此示例展示 Worker 处理 CPU 密集型任务的核心优势。
</div>
</div>
<script>
// ====== 辅助函数 ======
function logTo(el, text, cls = '') {
const div = document.createElement('div');
div.className = `result-line ${cls}`;
div.textContent = text;
el.innerHTML = '';
el.appendChild(div);
}
function appendLog(el, text, cls = '') {
const div = document.createElement('div');
div.className = `result-line ${cls}`;
div.textContent = text;
el.appendChild(div);
el.scrollTop = el.scrollHeight;
}
// ====== 斐波那契 ======
// 递归实现(非常慢,用于演示阻塞)
function fibRecursive(n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
function calcFibonacci(useWorker) {
const n = parseInt(document.getElementById('fibN').value);
const resultEl = document.getElementById('fibResult');
const busyEl = document.getElementById('fibBusy');
document.getElementById('fibBtnMain').disabled = true;
document.getElementById('fibBtnWorker').disabled = true;
busyEl.style.display = 'inline-flex';
logTo(resultEl, `[${useWorker ? 'Worker' : '主线程'}] 计算斐波那契(${n})...`, 'warn');
const startTime = performance.now();
if (!useWorker) {
// 主线程计算(会阻塞)
setTimeout(() => {
const result = fibRecursive(n);
const elapsed = performance.now() - startTime;
document.getElementById('fibAns').textContent = result.toLocaleString();
document.getElementById('fibTime').textContent = elapsed.toFixed(1);
document.getElementById('fibMethod').textContent = '主线程';
logTo(resultEl, `fib(${n}) = ${result.toLocaleString()}\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('fibBtnMain').disabled = false;
document.getElementById('fibBtnWorker').disabled = false;
}, 10);
} else {
// Worker 计算
const code = `
self.onmessage = function(e) {
const n = e.data;
function fib(n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); }
const result = fib(n);
self.postMessage({ n: n, result: result, time: performance.now() });
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const elapsed = performance.now() - startTime;
document.getElementById('fibAns').textContent = e.data.result.toLocaleString();
document.getElementById('fibTime').textContent = elapsed.toFixed(1);
document.getElementById('fibMethod').textContent = 'Worker';
logTo(resultEl, `fib(${e.data.n}) = ${e.data.result.toLocaleString()}\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('fibBtnMain').disabled = false;
document.getElementById('fibBtnWorker').disabled = false;
worker.terminate();
};
worker.postMessage(n);
}
}
// ====== 素数计算 ======
function isPrime(num) {
if (num < 2) return false;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
}
function countPrimes(max) {
let count = 0;
for (let i = 2; i <= max; i++) {
if (isPrime(i)) count++;
}
return count;
}
function calcPrimes(useWorker) {
const max = parseInt(document.getElementById('primeMax').value);
const resultEl = document.getElementById('primeResult');
const busyEl = document.getElementById('primeBusy');
document.getElementById('primeBtnMain').disabled = true;
document.getElementById('primeBtnWorker').disabled = true;
busyEl.style.display = 'inline-flex';
logTo(resultEl, `[${useWorker ? 'Worker' : '主线程'}] 统计 1-${max.toLocaleString()} 内的素数...`, 'warn');
const startTime = performance.now();
if (!useWorker) {
setTimeout(() => {
const count = countPrimes(max);
const elapsed = performance.now() - startTime;
document.getElementById('primeCount').textContent = count.toLocaleString();
document.getElementById('primeTime').textContent = elapsed.toFixed(1);
document.getElementById('primeMethod').textContent = '主线程';
logTo(resultEl, `1 到 ${max.toLocaleString()} 内有 ${count.toLocaleString()} 个素数\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('primeBtnMain').disabled = false;
document.getElementById('primeBtnWorker').disabled = false;
}, 10);
} else {
const code = `
self.onmessage = function(e) {
const max = e.data;
let count = 0;
for (let i = 2; i <= max; i++) {
let prime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) { prime = false; break; }
}
if (prime) count++;
}
self.postMessage({ max: max, count: count });
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = function(e) {
const elapsed = performance.now() - startTime;
document.getElementById('primeCount').textContent = e.data.count.toLocaleString();
document.getElementById('primeTime').textContent = elapsed.toFixed(1);
document.getElementById('primeMethod').textContent = 'Worker';
logTo(resultEl, `1 到 ${e.data.max.toLocaleString()} 内有 ${e.data.count.toLocaleString()} 个素数\n耗时: ${elapsed.toFixed(1)}ms`, 'highlight');
busyEl.style.display = 'none';
document.getElementById('primeBtnMain').disabled = false;
document.getElementById('primeBtnWorker').disabled = false;
worker.terminate();
};
worker.postMessage(max);
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【5】Worker 终止与超时控制</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: terminate() / setTimeout 自动终止 Worker
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #0f0f23; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 880px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #1a1a2e; border-radius: 10px; padding: 24px;
margin-bottom: 20px; border: 1px solid #2d2d4a;
}
.card-title {
font-size: 15px; font-weight: 600; color: #ff6b6b;
border-left: 3px solid #ff6b6b; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(255,107,107,0.1); border-radius: 6px; padding: 12px 16px;
font-size: 13px; line-height: 1.6; color: #ffa07a; margin-bottom: 16px;
}
.control-row { display: flex; gap: 10px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
label { font-size: 13px; color: #aaa; }
input[type="number"], input[type="text"] {
padding: 8px 12px; background: #0d0d1a; border: 1px solid #333;
color: #fff; border-radius: 6px; font-size: 13px; width: 120px;
}
input:focus { outline: none; border-color: #ff6b6b; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-red { background: #ff6b6b; color: white; }
.btn-red:hover { background: #ee5a24; }
.btn-orange { background: #ffa502; color: #1a1a2e; }
.btn-orange:hover { background: #e69500; }
.btn-blue { background: #3498db; color: white; }
.btn-blue:hover { background: #2980b9; }
.btn-green { background: #2ed573; color: #1a1a2e; }
.btn-green:hover { background: #26af61; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
.timeline {
position: relative; padding: 20px 0; min-height: 80px;
}
.timeline-bar {
height: 40px; background: linear-gradient(90deg, #2ed573, #7bed9f);
border-radius: 20px; transition: width 0.1s linear; width: 0%;
display: flex; align-items: center; justify-content: center;
color: #1a1a2e; font-size: 12px; font-weight: 600; min-width: 30px;
}
.timeline-bar.killed {
background: linear-gradient(90deg, #ff6b6b, #ee5a24);
}
.timeline-label {
display: flex; justify-content: space-between; font-size: 11px;
color: #666; margin-top: 6px;
}
.status-grid {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 16px;
}
.stat-card {
background: #0d0d1a; border-radius: 8px; padding: 14px; text-align: center;
}
.stat-value { font-size: 20px; font-weight: 700; }
.stat-value.running { color: #2ed573; }
.stat-value.stopped { color: #ff6b6b; }
.stat-value.idle { color: #888; }
.stat-label { font-size: 11px; color: #666; margin-top: 4px; }
.log-console {
background: #0a0a15; border-radius: 8px; padding: 14px;
font-family: monospace; font-size: 12px; max-height: 250px;
overflow-y: auto; color: #aaa; line-height: 1.7;
}
.log-line { padding: 2px 0; }
.log-line.ok { color: #2ed573; }
.log-line.warn { color: #ffa502; }
.log-line.err { color: #ff6b6b; }
.log-line.info { color: #70a1ff; }
.method-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
@media (max-width: 600px) { .method-cards { grid-template-columns: 1fr; } }
.method-card {
background: #0d0d1a; border-radius: 8px; padding: 16px;
border: 1px solid #2d2d4a;
}
.method-card h4 { color: #ccc; font-size: 14px; margin-bottom: 8px; }
.method-card code {
display: block; background: #1a1a2e; padding: 10px; border-radius: 6px;
font-size: 12px; color: #70a1ff; white-space: pre-wrap; line-height: 1.5;
}
.method-card p { font-size: 12px; color: #777; margin-top: 8px; line-height: 1.5; }
.compat-note {
background: rgba(255,193,7,0.08); border: 1px solid rgba(255,193,7,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⏹️ Worker 终止与超时控制</h1>
<p>terminate() 强制终止 / setTimeout 自动超时终止</p>
</div>
<div class="card">
<div class="card-title">🎛️ 终止控制面板</div>
<div class="info-banner">
💡 <strong>两种终止方式:</strong><br>
① <code>worker.terminate()</code> — 主线程立即强制终止 Worker(不可恢复)<br>
② <code>self.close()</code> — Worker 自身主动关闭(优雅退出)<br>
③ 超时自动终止 — 主线程设置定时器,超时后调用 terminate()
</div>
<div class="control-row">
<label>循环次数 (模拟耗时):</label>
<input type="number" id="loopCount" value="100000000" min="1000000" step="10000000" />
<label>超时时间 (ms):</label>
<input type="number" id="timeoutMs" value="3000" min="500" step="500" />
</div>
<div class="control-row">
<button class="btn btn-green" id="startBtn" onclick="startWorker()">▶ 启动 Worker</button>
<button class="btn btn-red" id="terminateBtn" onclick="manualTerminate()" disabled>⛹ 手动 terminate()</button>
<button class="btn btn-orange" id="timeoutBtn" onclick="startWithTimeout()" disabled>⏱ 带超时启动</button>
<button class="btn btn-blue" onclick="clearLogs()">🧹 清空日志</button>
</div>
<!-- 进度时间线 -->
<div style="margin-top: 20px;">
<div style="font-size: 13px; color: #aaa; margin-bottom: 8px;">执行进度</div>
<div class="timeline">
<div class="timeline-bar" id="progressBar">0%</div>
</div>
<div class="timeline-label">
<span>开始</span>
<span id="timeLabel">--</span>
<span>完成</span>
</div>
</div>
<div class="status-grid">
<div class="stat-card">
<div class="stat-value idle" id="statusValue">空闲</div>
<div class="stat-label">Worker 状态</div>
</div>
<div class="stat-card">
<div class="stat-value idle" id="elapsedValue">-</div>
<div class="stat-label">已运行时间</div>
</div>
<div class="stat-card">
<div class="stat-value idle" id="progressValue">0%</div>
<div class="stat-label">计算进度</div>
</div>
</div>
<div class="log-console" id="logConsole"></div>
</div>
<div class="card">
<div class="card-title">📖 终止方法对比</div>
<div class="method-cards">
<div class="method-card">
<h4>🔴 worker.terminate() — 外部强制终止</h4>
<code>// 在主线程中调用
const worker = new Worker('script.js');
worker.terminate(); // 立即终止,不可恢复</code>
<p>特点:主线程调用,立即终止,Worker 内部代码不会继续执行。已发送但未处理的消息会丢失。</p>
</div>
<div class="method-card">
<h4>🟢 self.close() — Worker 自身退出</h4>
<code>// 在 Worker 脚本内部调用
self.onmessage = function(e) {
// 完成工作后
self.close(); // 优雅退出
};</code>
<p>特点:Worker 主动调用,可确保清理工作完成后退出。推荐在正常结束时使用。</p>
</div>
</div>
<div class="compat-note">
⚠️ <strong>兼容性提示:</strong><code>terminate()</code> 和 <code>self.close()</code> 在所有支持 Web Workers 的浏览器中均可用。
</div>
</div>
</div>
<script>
const logEl = document.getElementById('logConsole');
let currentWorker = null;
let timeoutTimer = null;
let elapsedTimer = null;
let startTime = 0;
function log(msg, type = '') {
const div = document.createElement('div');
div.className = `log-line ${type}`;
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
logEl.appendChild(div);
logEl.scrollTop = logEl.scrollHeight;
}
function clearLogs() { logEl.innerHTML = ''; }
function setStatus(text, cls) {
const el = document.getElementById('statusValue');
el.textContent = text;
el.className = `stat-value ${cls}`;
}
function setButtons(running) {
document.getElementById('startBtn').disabled = running;
document.getElementById('terminateBtn').disabled = !running;
document.getElementById('timeoutBtn').disabled = !running;
}
function resetProgress() {
document.getElementById('progressBar').style.width = '0%';
document.getElementById('progressBar').textContent = '0%';
document.getElementById('progressBar').className = 'timeline-bar';
document.getElementById('elapsedValue').textContent = '-';
document.getElementById('elapsedValue').className = 'stat-value idle';
document.getElementById('progressValue').textContent = '0%';
document.getElementById('timeLabel').textContent = '--';
}
function startElapsedTimer() {
startTime = performance.now();
elapsedTimer = setInterval(() => {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value running';
document.getElementById('timeLabel').textContent = `${elapsed}s`;
}, 100);
}
function stopElapsedTimer() {
if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
}
// ====== Worker 代码 ======
const workerCode = `
self.onmessage = function(e) {
const total = e.data.total;
self.postMessage({ type: 'start', total: total });
let sum = 0;
for (let i = 0; i <= total; i++) {
sum += i;
// 每 2% 报告一次进度
if (i % Math.floor(total / 50) === 0) {
self.postMessage({
type: 'progress',
percent: Math.round((i / total) * 100),
current: i
});
}
}
self.postMessage({ type: 'done', result: sum });
};
`;
function createWorkerInstance() {
const blob = new Blob([workerCode], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
}
function startWorker() {
const total = parseInt(document.getElementById('loopCount').value) || 100000000;
resetProgress();
setButtons(true);
setStatus('运行中', 'running');
currentWorker = createWorkerInstance();
startElapsedTimer();
log(`启动 Worker,任务: 循环 ${total.toLocaleString()} 次`, 'ok');
currentWorker.onmessage = function(e) {
const d = e.data;
if (d.type === 'start') {
log(`[Worker] 开始执行,总计 ${d.total.toLocaleString()} 次迭代`, 'info');
} else if (d.type === 'progress') {
const bar = document.getElementById('progressBar');
bar.style.width = `${d.percent}%`;
bar.textContent = `${d.percent}%`;
document.getElementById('progressValue').textContent = `${d.percent}%`;
} else if (d.type === 'done') {
stopElapsedTimer();
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
setStatus('已完成', 'stopped');
setButtons(false);
log(`[Worker] 完成! 结果 sum=${d.result.toLocaleString()} | 耗时 ${elapsed}s`, 'ok');
currentWorker = null;
}
};
currentWorker.onerror = function(e) {
stopElapsedTimer();
log(`[Worker 错误] ${e.message}`, 'err');
setStatus('错误', 'stopped');
setButtons(false);
e.preventDefault();
currentWorker = null;
};
currentWorker.postMessage({ total: total });
}
function manualTerminate() {
if (!currentWorker) return;
currentWorker.terminate();
stopElapsedTimer();
if (timeoutTimer) { clearTimeout(timeoutTimer); timeoutTimer = null; }
const bar = document.getElementById('progressBar');
bar.classList.add('killed');
bar.textContent = '已终止 ⛹';
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value stopped';
setStatus('已终止', 'stopped');
setButtons(false);
log(`⛹ 手动调用 terminate() 终止 Worker (运行 ${elapsed}s)`, 'warn');
currentWorker = null;
}
function startWithTimeout() {
const total = parseInt(document.getElementById('loopCount').value) || 100000000;
const timeout = parseInt(document.getElementById('timeoutMs').value) || 3000;
resetProgress();
setButtons(true);
setStatus('运行中 (有超时)', 'running');
currentWorker = createWorkerInstance();
startElapsedTimer();
log(`启动 Worker (带 ${timeout}ms 超时),任务: ${total.toLocaleString()} 次循环`, 'ok');
// 设置超时定时器
timeoutTimer = setTimeout(() => {
if (!currentWorker) return;
currentWorker.terminate();
stopElapsedTimer();
const bar = document.getElementById('progressBar');
bar.classList.add('killed');
bar.textContent = `⏱ 超时终止 (${timeout}ms)`;
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
document.getElementById('elapsedValue').className = 'stat-value stopped';
setStatus('超时终止', 'stopped');
setButtons(false);
log(`⏱ 超时! ${timeout}ms 后自动调用 terminate() (已运行 ${elapsed}s)`, 'err');
currentWorker = null;
}, timeout);
currentWorker.onmessage = function(e) {
const d = e.data;
if (d.type === 'start') {
log(`[Worker] 开始执行`, 'info');
} else if (d.type === 'progress') {
const bar = document.getElementById('progressBar');
bar.style.width = `${d.percent}%`;
bar.textContent = `${d.percent}%`;
document.getElementById('progressValue').textContent = `${d.percent}%`;
} else if (d.type === 'done') {
clearTimeout(timeoutTimer);
timeoutTimer = null;
stopElapsedTimer();
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
document.getElementById('elapsedValue').textContent = `${elapsed}s`;
setStatus('已完成', 'stopped');
setButtons(false);
log(`[Worker] 在超时前完成! 耗时 ${elapsed}s`, 'ok');
currentWorker = null;
}
};
currentWorker.onerror = function(e) {
clearTimeout(timeoutTimer);
timeoutTimer = null;
stopElapsedTimer();
log(`[Worker 错误] ${e.message}`, 'err');
setStatus('错误', 'stopped');
setButtons(false);
e.preventDefault();
currentWorker = null;
};
currentWorker.postMessage({ total: total });
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【6】Shared Worker 多页面共享状态</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Shared Worker 多页面/iframe 共享一个 SharedWorker
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; min-height: 100vh;
}
.container { max-width: 1100px; margin: 0 auto; }
.header {
text-align: center; padding: 28px; background: rgba(255,255,255,0.1);
border-radius: 12px; margin-bottom: 24px; backdrop-filter: blur(10px);
}
.header h1 { font-size: 24px; margin-bottom: 8px; }
.header p { opacity: 0.85; font-size: 14px; }
.warning-banner {
background: rgba(255,193,7,0.2); border: 1px solid rgba(255,193,7,0.4);
border-radius: 8px; padding: 14px 18px; font-size: 13px; line-height: 1.6;
color: #fff3cd; margin-bottom: 20px;
}
.iframes-container {
display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
}
@media (max-width: 800px) { .iframes-container { grid-template-columns: 1fr; } }
.iframe-wrapper {
background: white; border-radius: 12px; overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.iframe-header {
background: #f8f9fa; padding: 10px 16px; font-size: 13px; font-weight: 600;
color: #333; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 8px;
}
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot.blue { background: #3498db; }
.dot.green { background: #27ae60; }
iframe {
width: 100%; height: 420px; border: none; display: block;
}
.shared-state-panel {
background: rgba(255,255,255,0.1); border-radius: 12px; padding: 20px;
margin-top: 20px; backdrop-filter: blur(10px);
}
.shared-state-title {
font-size: 15px; font-weight: 600; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
.state-display {
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.state-item {
background: rgba(255,255,255,0.1); border-radius: 8px; padding: 12px;
text-align: center;
}
.state-value { font-size: 22px; font-weight: 700; }
.state-label { font-size: 11px; opacity: 0.75; margin-top: 4px; }
.compat-note {
background: rgba(231,76,60,0.15); border: 1px solid rgba(231,76,60,0.35);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffcdd2;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔗 Shared Worker 多页面共享</h1>
<p>两个 iframe 共享同一个 SharedWorker,实现跨页面状态同步</p>
</div>
<div class="warning-banner">
⚠️ <strong>Safari 不支持 Shared Worker!</strong>请在 Chrome、Firefox 或 Edge 中打开此页面以获得完整体验。
Shared Worker 允许同一浏览器的多个标签页/iframe 共享同一个 Worker 实例。
</div>
<div class="iframes-container">
<div class="iframe-wrapper">
<div class="iframe-header"><span class="dot blue"></span> 页面 A (Client A)</div>
<iframe id="frameA" src="about:blank"></iframe>
</div>
<div class="iframe-wrapper">
<div class="iframe-header"><span class="dot green"></span> 页面 B (Client B)</div>
<iframe id="frameB" src="about:blank"></iframe>
</div>
</div>
<div class="shared-state-panel">
<div class="shared-state-title">📊 Shared Worker 共享状态</div>
<div class="state-display">
<div class="state-item">
<div class="state-value" id="connCount">0</div>
<div class="state-label">连接数</div>
</div>
<div class="state-item">
<div class="state-value" id="totalCount">0</div>
<div class="state-label">消息总数</div>
</div>
<div class="state-item">
<div class="state-value" id="lastSender">-</div>
<div class="state-label">最后发送者</div>
</div>
<div class="state-item">
<div class="state-value" id="sharedCounter">0</div>
<div class="state-label">共享计数器</div>
</div>
</div>
</div>
<div class="compat-note">
🌐 <strong>兼容性:</strong>Chrome 4+ ✅ | Firefox 29+ ✅ | Edge 79+ ✅ | Safari ❌ 不支持
</div>
</div>
<!-- ========== iframe 子页面模板 ========== -->
<script>
// 创建 iframe 内容 HTML
const iframeHTML = (color, label) => `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px; background: #fafafa; color: #333; }
.client-header { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: ${color}; }
.input-area { display: flex; gap: 8px; margin-bottom: 12px; }
input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; }
input:focus { outline: none; border-color: ${color}; }
button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; background: ${color}; color: white; transition: opacity 0.2s; }
button:hover { opacity: 0.85; }
.message-list { background: #f0f0f0; border-radius: 8px; padding: 12px; max-height: 240px; overflow-y: auto; font-size: 12px; }
.msg-item { padding: 5px 8px; margin-bottom: 4px; border-radius: 4px; animation: fadeIn 0.2s; }
.msg-item.from-me { background: #dbeafe; color: #1e40af; text-align: right; }
.msg-item.from-other { background: #dcfce7; color: #166534; }
.msg-item.system { background: #fef3c7; color: #92400e; font-style: italic; font-size: 11px; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
.status-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; background: #d4edda; color: #155724; margin-bottom: 10px; }
.counter-btn { margin-top: 8px; width: 100%; background: ${color} !important; }
</style>
</head>
<body>
<div class="client-header">${label}</div>
<div class="status-badge" id="status">⏳ 连接中...</div>
<div class="input-area">
<input type="text" id="msgInput" placeholder="输入消息广播给所有客户端..." />
<button onclick="sendMessage()">发送</button>
</div>
<button class="counter-btn" onclick="incrementCounter()">➕ 共享计数器 +1</button>
<div class="message-list" id="msgList"></div>
<script>
const msgList = document.getElementById('msgList');
const statusEl = document.getElementById('status');
let myPort = null;
function addMsg(text, type) {
const div = document.createElement('div');
div.className = 'msg-item ' + type;
div.textContent = text;
msgList.appendChild(div);
msgList.scrollTop = msgList.scrollHeight;
}
// 检查是否支持 SharedWorker
if ('SharedWorker' in window) {
try {
// 使用内联 Blob URL 创建 SharedWorker
const sharedCode = \`
let connections = [];
let messageCount = 0;
let sharedCounter = 0;
onconnect = function(e) {
const port = e.ports[0];
connections.push(port);
port.postMessage({
type: 'welcome',
clientName: '${label}',
connectionCount: connections.length,
sharedCounter: sharedCounter,
messageCount: messageCount
});
port.onmessage = function(event) {
const data = event.data;
if (data.type === 'broadcast') {
messageCount++;
sharedCounter = data.counter || sharedCounter;
// 广播给所有连接的客户端
connections.forEach(function(p) {
p.postMessage({
type: 'broadcast',
from: '${label}',
text: data.text,
timestamp: Date.now(),
messageCount: messageCount,
sharedCounter: sharedCounter
});
});
// 通知父窗口更新状态
if (typeof window.parent !== 'undefined') {
window.parent.postMessage({
type: 'stateUpdate',
connCount: connections.length,
totalCount: messageCount,
lastSender: '${label}',
counter: sharedCounter
}, '*');
}
}
if (data.type === 'increment') {
sharedCounter++;
messageCount++;
connections.forEach(function(p) {
p.postMessage({
type: 'counterUpdate',
from: '${label}',
counter: sharedCounter,
messageCount: messageCount
});
});
if (typeof window.parent !== 'undefined') {
window.parent.postMessage({
type: 'stateUpdate',
connCount: connections.length,
totalCount: messageCount,
lastSender: '${label}',
counter: sharedCounter
}, '*');
}
}
};
port.start();
};
// 清理断开的连接
setInterval(function() {
// 简单的心跳检测可以在这里添加
}, 5000);
\`;
const blob = new Blob([sharedCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const sharedWorker = new SharedWorker(url, 'demo-shared-worker');
myPort = sharedWorker.port;
myPort.start();
myPort.onmessage = function(e) {
const data = e.data;
if (data.type === 'welcome') {
statusEl.textContent = '✅ 已连接 ( #' + data.connectionCount + ' )';
addMsg('欢迎! 当前 ' + data.connectionCount + ' 个客户端在线, 计数器=' + data.sharedCounter, 'system');
notifyParent(data);
} else if (data.type === 'broadcast') {
addMsg('[' + data.from + '] ' + data.text, data.from === '${label}' ? 'from-me' : 'from-other');
updateLocalState(data);
} else if (data.type === 'counterUpdate') {
addMsg('[' + data.from + '] 计数器 → ' + data.counter, 'from-other');
updateLocalState(data);
}
};
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !myPort) return;
myPort.postMessage({ type: 'broadcast', text: text });
addMsg('我: ' + text, 'from-me');
input.value = '';
}
function incrementCounter() {
if (!myPort) return;
myPort.postMessage({ type: 'increment' });
}
function notifyParent(data) {
window.parent.postMessage({
type: 'stateUpdate',
connCount: data.connectionCount,
totalCount: data.messageCount || 0,
lastSender: '${label}',
counter: data.sharedCounter || 0
}, '*');
}
function updateLocalState(data) {
window.parent.postMessage({
type: 'stateUpdate',
connCount: data.connCount || 0,
totalCount: data.messageCount || 0,
lastSender: data.from || '-',
counter: data.counter || data.sharedCounter || 0
}, '*');
}
document.getElementById('msgInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
} catch(err) {
statusEl.textContent = '❌ 错误: ' + err.message;
addMsg('SharedWorker 创建失败: ' + err.message, 'system');
}
} else {
statusEl.textContent = '❌ 浏览器不支持 SharedWorker';
addMsg('您的浏览器不支持 SharedWorker (Safari 不支持)', 'system');
}
<\/script>
</body>
</html>`;
// 将 HTML 写入两个 iframe
const frameA = document.getElementById('frameA');
const frameB = document.getElementById('frameB');
frameA.srcdoc = iframeHTML('#3498db', 'Client-A');
frameB.srcdoc = iframeHTML('#27ae60', 'Client-B');
// 监听子页面的状态更新
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'stateUpdate') {
document.getElementById('connCount').textContent = e.data.connCount;
document.getElementById('totalCount').textContent = e.data.totalCount;
document.getElementById('lastSender').textContent = e.data.lastSender;
document.getElementById('sharedCounter').textContent = e.data.counter;
}
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【7】Web Worker 内联方式对比</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Blob URL vs 外部 JS 文件 vs importScripts 三种创建方式对比
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #f5f7fa; color: #333;
}
.container { max-width: 1000px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #11998e, #38ef7d);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.methods-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
@media (max-width: 768px) { .methods-grid { grid-template-columns: 1fr; } }
.method-card {
background: white; border-radius: 10px; overflow: hidden;
box-shadow: 0 2px 12px rgba(0,0,0,0.06); transition: transform 0.2s;
}
.method-card:hover { transform: translateY(-3px); }
.method-header {
padding: 14px 16px; font-weight: 600; font-size: 14px; color: white;
display: flex; align-items: center; gap: 8px;
}
.method-header.blob { background: #6c5ce7; }
.method-header.external { background: #0984e3; }
.method-header.import { background: #00b894; }
.method-body { padding: 16px; }
.code-block {
background: #1e1e1e; border-radius: 6px; padding: 12px;
font-family: monospace; font-size: 11px; color: #d4d4d4; overflow-x: auto;
line-height: 1.6; white-space: pre; margin-bottom: 12px;
}
.code-keyword { color: #569cd6; }
.code-string { color: #ce9178; }
.code-comment { color: #6a9955; }
.run-btn {
width: 100%; padding: 10px; border: none; border-radius: 6px;
cursor: pointer; font-size: 13px; font-weight: 600; color: white;
transition: all 0.2s;
}
.run-btn.blob { background: #6c5ce7; }
.run-btn.external { background: #0984e3; }
.run-btn.import { background: #00b894; }
.run-btn:hover { opacity: 0.85; transform: scale(1.02); }
.run-btn:active { transform: scale(0.98); }
.result-mini {
margin-top: 10px; padding: 10px; border-radius: 6px;
font-size: 12px; display: none;
}
.result-mini.success { display: block; background: #d4edda; color: #155724; }
.result-mini.error { display: block; background: #f8d7da; color: #721c24; }
/* 对比表格 */
.compare-section {
background: white; border-radius: 10px; padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.section-title {
font-size: 16px; font-weight: 600; color: #333;
border-left: 4px solid #38ef7d; padding-left: 12px; margin-bottom: 16px;
}
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; color: #555; }
tr:hover td { background: #fafbfd; }
.badge {
display: inline-block; padding: 2px 10px; border-radius: 10px;
font-size: 11px; font-weight: 600;
}
.badge-good { background: #d4edda; color: #155724; }
.badge-mid { background: #fff3cd; color: #856404; }
.badge-bad { background: #f8d7da; color: #721c24; }
.compat-box {
background: #e8f5e9; border-left: 4px solid #4caf50;
padding: 14px 16px; border-radius: 4px; font-size: 13px;
line-height: 1.6; color: #2e7d32; margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔄 Web Worker 创建方式对比</h1>
<p>Blob URL 内联 vs 外部 JS 文件 vs importScripts 加载外部库</p>
</div>
<div class="methods-grid">
<!-- 方式一:Blob URL -->
<div class="method-card">
<div class="method-header blob">📦 方式一: Blob URL (内联)</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// 将 Worker 代码作为字符串</span>
<span class="code-keyword">const</span> code = <span class="code-string">'self.onmessage=e=>...'</span>;
<span class="code-keyword">const</span> blob = <span class="code-keyword">new</span> Blob([code]);
<span class="code-keyword">const</span> url = URL.createObjectURL(blob);
<span class="code-keyword">const</span> w = <span class="code-keyword">new</span> Worker(url);</div>
<button class="run-btn blob" onclick="testBlob()">▶ 运行测试</button>
<div class="result-mini success" id="resultBlob"></div>
</div>
</div>
<!-- 方式二:外部文件 -->
<div class="method-card">
<div class="method-header external">📁 方式二: 外部 JS 文件</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// 引用外部 .js 文件路径</span>
<span class="code-comment">// worker.js 文件内容:</span>
<span class="code-comment">// self.onmessage=function(e){...}</span>
<span class="code-keyword">const</span> w = <span class="code-keyword">new</span> Worker(<span class="code-string">'./worker.js'</span>);
w.postMessage(data);</div>
<button class="run-btn external" onclick="testExternal()">▶ 运行测试</button>
<div class="result-mini success" id="resultExternal"></div>
</div>
</div>
<!-- 方式三:importScripts -->
<div class="method-card">
<div class="method-header import">📚 方式三: importScripts</div>
<div class="method-body">
<div class="code-block"><span class="code-comment">// Worker 中加载外部脚本</span>
<span class="code-comment">// main-worker.js:</span>
importScripts(<span class="code-string">'utils.js'</span>, <span class="code-string">'math.js'</span>);
self.onmessage = <span class="code-keyword">function</span>(e) {
<span class="code-comment">// 可使用 utils.js/math.js 的函数</span>
self.postMessage(calculate(e.data));
};</div>
<button class="run-btn import" onclick="testImportScripts()">▶ 运行测试</button>
<div class="result-mini success" id="resultImport"></div>
</div>
</div>
</div>
<!-- 对比表格 -->
<div class="compare-section">
<div class="section-title">📊 三种方式详细对比</div>
<table>
<thead>
<tr>
<th>特性</th>
<th>Blob URL (内联)</th>
<th>外部 JS 文件</th>
<th>importScripts</th>
</tr>
</thead>
<tbody>
<tr>
<td>文件数量</td>
<td>单文件 (HTML+JS一体)</td>
<td>需要额外 .js 文件</td>
<td>主文件 + 依赖文件</td>
</tr>
<tr>
<td>部署便利性</td>
<td><span class="badge badge-good">最佳</span></td>
<td><span class="badge badge-mid">一般</span></td>
<td><span class="badge badge-bad">较复杂</span></td>
</tr>
<tr>
<td>调试体验</td>
<td><span class="badge badge-bad">较差</span> (Blob URL 难调试)</td>
<td><span class="badge badge-good">最佳</span> (独立源文件)</td>
<td><span class="badge badge-mid">中等</span></td>
</tr>
<tr>
<td>代码复用</td>
<td><span class="badge badge-bad">无</span></td>
<td><span class="badge badge-good">好</span></td>
<td><span class="badge badge-good">优秀</span></td>
</tr>
<tr>
<td>缓存策略</td>
<td><span class="badge badge-bad">无缓存</span></td>
<td><span class="badge badge-good">浏览器缓存</span></td>
<td><span class="badge badge-good">各文件独立缓存</span></td>
</tr>
<tr>
<td>CORS 限制</td>
<td><span class="badge badge-good">无</span> (同源)</td>
<td><span class="badge badge-mid">需同源或 CORS</span></td>
<td><span class="badge badge-mid">依赖文件需 CORS</span></td>
</tr>
<tr>
<td>适用场景</td>
<td>演示、原型、单文件工具</td>
<td>生产环境、大型项目</td>
<td>需使用第三方库的 Worker</td>
</tr>
<tr>
<td>性能开销</td>
<td><span class="badge badge-mid">每次创建新 Blob</span></td>
<td><span class="badge badge-good">低</span></td>
<td><span class="badge badge-mid">加载依赖有延迟</span></td>
</tr>
</tbody>
</table>
<div class="compat-box">
✅ <strong>兼容性提示:</strong>三种方式在现代浏览器中均完全支持。<br>
• <strong>Blob URL</strong> 最适合示例和演示(如本文件集的所有示例)<br>
• <strong>外部文件</strong> 是生产环境的首选方案<br>
• <strong>importScripts</strong> 适合需要在 Worker 中使用第三方库的场景
</div>
</div>
</div>
<script>
// ====== 测试函数 ======
// 方式一:Blob URL
function testBlob() {
const resultEl = document.getElementById('resultBlob');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
const code = `
self.onmessage = function(e) {
const n = e.data;
let sum = 0;
for (let i = 1; i <= n; i++) sum += i;
self.postMessage({ method: 'Blob URL', result: sum, n: n });
};
`;
try {
const blob = new Blob([code], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`计算 1~${d.n} 的累加 = <strong>${d.result.toLocaleString()}</strong><br>` +
`耗时: ~<small>即时完成</small>`;
worker.terminate();
URL.revokeObjectURL(url);
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(100000);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
// 方式二:外部文件 (模拟)
function testExternal() {
const resultEl = document.getElementById('resultExternal');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
// 模拟外部文件:先创建一个临时的外部 Worker 文件
const externalCode = `
// 这是 "external-worker.js" 的内容
self.onmessage = function(e) {
const n = e.data;
let product = 1;
for (let i = 1; i <= Math.min(n, 20); i++) product *= i;
self.postMessage({
method: 'External File',
result: product,
n: n,
note: '模拟从外部 .js 文件加载'
});
};
`;
// 在实际项目中,这里应该是:
// const worker = new Worker('./external-worker.js');
// 为了让本示例可独立运行,我们用 Blob URL 模拟
try {
const blob = new Blob([externalCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`计算 1~${Math.min(d.n,20)} 的阶乘 = <strong>${d.result.toLocaleString()}</strong><br>` +
`<small>${d.note}</small>`;
worker.terminate();
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(15);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
// 方式三:importScripts
function testImportScripts() {
const resultEl = document.getElementById('resultImport');
resultEl.className = 'result-mini error';
resultEl.textContent = '测试中...';
// 模拟外部依赖库
const utilsLib = `
// utils.js - 工具函数库
var MathUtils = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) {
var temp = a + b;
a = b;
b = temp;
}
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
},
formatNumber: function(n) {
return n.toLocaleString();
}
};
`;
const mainWorker = `
// 通过 importScripts 加载外部库
importScripts('data:text/javascript,' + encodeURIComponent(\`
var MathUtils = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) { var t = a+b; a = b; b = t; }
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) { if (num%i===0) return false; }
return true;
}
};
\`));
self.onmessage = function(e) {
var n = e.data;
var fibResult = MathUtils.fibonacci(n);
var primeCheck = MathUtils.isPrime(fibResult);
self.postMessage({
method: 'importScripts',
fibN: n,
fibResult: fibResult,
isPrime: primeCheck,
note: '通过 importScripts 加载了 MathUtils 库'
});
};
`;
try {
const blob = new Blob([mainWorker], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
resultEl.className = 'result-mini success';
resultEl.innerHTML = `✅ 成功! 方式: ${d.method}<br>` +
`fib(${d.fibN}) = <strong>${d.fibResult.toLocaleString()}</strong><br>` +
`结果是否为素数: <strong>${d.isPrime ? '是 ✓' : '否 ✗'}</strong><br>` +
`<small>${d.note}</small>`;
worker.terminate();
};
worker.onerror = function(e) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 失败: ${e.message}`;
e.preventDefault();
};
worker.postMessage(40);
} catch (err) {
resultEl.className = 'result-mini error';
resultEl.textContent = `❌ 异常: ${err.message}`;
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【8】Worker importScripts 加载外部库</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Worker 中使用 importScripts 加载外部脚本/库
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #1a1a2e; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 960px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #f093fb, #f5576c);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #16213e; border-radius: 10px; padding: 24px;
margin-bottom: 20px; border: 1px solid #0f3460;
}
.card-title {
font-size: 15px; font-weight: 600; color: #f093fb;
border-left: 3px solid #f093fb; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(240,147,251,0.1); border-radius: 6px; padding: 12px 16px;
font-size: 13px; line-height: 1.6; color: #f5a6ff; margin-bottom: 16px;
}
/* 库展示 */
.libs-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 16px; }
.lib-card {
background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 14px;
transition: border-color 0.2s;
}
.lib-card:hover { border-color: #f093fb; }
.lib-name { font-weight: 600; font-size: 13px; color: #f093fb; margin-bottom: 4px; }
.lib-desc { font-size: 11px; color: #888; line-height: 1.5; }
.lib-status { display: inline-block; padding: 2px 8px; border-radius: 8px; font-size: 10px; margin-top: 6px; }
.lib-status.loaded { background: rgba(46,213,115,0.15); color: #2ed573; }
.lib-status.loading { background: rgba(255,165,2,0.15); color: #ffa502; }
.control-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }
label { font-size: 13px; color: #aaa; }
select, input {
padding: 8px 12px; background: #0a0a1a; border: 1px solid #333;
color: #fff; border-radius: 6px; font-size: 13px;
}
input:focus, select:focus { outline: none; border-color: #f093fb; }
.btn {
padding: 9px 18px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 500; transition: all 0.2s;
}
.btn-pink { background: linear-gradient(135deg, #f093fb, #f5576c); color: white; }
.btn-pink:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(240,147,251,0.3); }
.btn-pink:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
.result-panel {
background: #0a0a1a; border-radius: 8px; padding: 16px;
font-family: monospace; font-size: 13px; min-height: 120px;
max-height: 300px; overflow-y: auto; color: #ccc; line-height: 1.7;
}
.result-line { padding: 3px 0; }
.result-line.ok { color: #2ed573; }
.result-line.err { color: #ff6b81; }
.result-line.info { color: #70a1ff; }
.result-line.highlight { color: #ffa502; font-weight: 600; }
/* 加载流程 */
.flow-diagram {
display: flex; align-items: center; justify-content: center; gap: 8px;
padding: 20px; flex-wrap: wrap;
}
.flow-node {
background: #0f3460; border: 1px solid #1a4a7a; border-radius: 8px;
padding: 12px 18px; text-align: center; font-size: 12px; color: #aaa;
min-width: 100px;
}
.flow-node.active { border-color: #f093fb; color: #f093fb; background: rgba(240,147,251,0.08); }
.flow-arrow { color: #444; font-size: 18px; }
.code-preview {
background: #0d1117; border-radius: 8px; padding: 14px; margin-top: 12px;
font-family: 'SF Mono', Monaco, Consolas, monospace; font-size: 11px;
color: #c9d1d9; line-height: 1.6; overflow-x: auto;
}
.code-preview .kw { color: #ff7b72; }
.code-preview .str { color: #a5d6ff; }
.code-preview .cm { color: #8b949e; }
.code-preview .fn { color: #d2a8ff; }
.compat-note {
background: rgba(255,193,7,0.08); border: 1px solid rgba(255,193,7,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffc107;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 importScripts 加载外部库</h1>
<p>在 Web Worker 中通过 importScripts 引入工具函数和第三方库</p>
</div>
<div class="card">
<div class="card-title">🔧 模拟加载的库</div>
<div class="libs-grid">
<div class="lib-card" id="lib-math">
<div class="lib-name">🔢 MathLib</div>
<div class="lib-desc">数学计算库:斐波那契、素数检测、阶乘、GCD、LCM</div>
<span class="lib-status loading" id="status-math">待加载</span>
</div>
<div class="lib-card" id="lib-string">
<div class="lib-name">📝 StringLib</div>
<div class="lib-desc">字符串处理:反转、驼峰转换、字频统计、Base64</div>
<span class="lib-status loading" id="status-string">待加载</span>
</div>
<div class="lib-card" id="lib-data">
<div class="lib-name">📊 DataLib</div>
<div class="lib-desc">数据处理:排序、去重、分组、聚合统计</div>
<span class="lib-status loading" id="status-data">待加载</span>
</div>
</div>
<div class="control-row">
<label>选择功能测试:</label>
<select id="funcSelect">
<option value="fibonacci">MathLib.fibonacci(n)</option>
<option value="isPrime">MathLib.isPrime(n)</option>
<option value="factorial">MathLib.factorial(n)</option>
<option value="reverseString">StringLib.reverse(str)</option>
<option value="wordCount">StringLib.wordCount(str)</option>
<option value="toCamelCase">StringLib.toCamelCase(str)</option>
<option value="unique">DataLib.unique(arr)</option>
<option value="groupBy">DataLib.groupBy(arr, key)</option>
<option value="stats">DataLib.stats(arr)</option>
</select>
<label>参数:</label>
<input type="text" id="funcParam" value="40" placeholder="输入参数..." />
<button class="btn btn-pink" onclick="runTest()" id="runBtn">▶ 执行 (importScripts)</button>
</div>
<div class="result-panel" id="resultPanel">
<div class="result-line info">[系统] 等待执行...</div>
</div>
</div>
<!-- 加载流程 -->
<div class="card">
<div class="card-title">📋 importScripts 加载流程</div>
<div class="flow-diagram">
<div class="flow-node active">主线程<br/>创建 Worker</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">Worker 启动</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">importScripts()<br/>同步加载脚本</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">库函数可用</div>
<div class="flow-arrow">→</div>
<div class="flow-node active">接收消息<br/>调用库函数</div>
</div>
<div class="code-preview"><span class="cm">// Worker 脚本中:</span>
<span class="kw">importScripts</span>(<span class="str">'math-lib.js'</span>, <span class="str">'string-lib.js'</span>, <span class="str">'data-lib.js'</span>);
<span class="cm">// 加载完成后,全局变量 MathLib/StringLib/DataLib 可用</span>
self.<span class="fn">onmessage</span> = <span class="kw">function</span>(e) {
<span class="kw">var</span> result = MathLib.<span class="fn">fibonacci</span>(e.data.n);
self.<span class="fn">postMessage</span>({ result: result });
};</div>
<div class="compat-note">
⚠️ <strong>注意:</strong><code>importScripts()</code> 是<strong>同步阻塞</strong>的,会暂停 Worker 执行直到所有脚本加载完成。
脚本按顺序依次加载,后加载的脚本可使用先加载脚本中的变量和函数。
</div>
</div>
</div>
<script>
const resultPanel = document.getElementById('resultPanel');
function log(msg, type = '') {
const div = document.createElement('div');
div.className = `result-line ${type}`;
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
resultPanel.appendChild(div);
resultPanel.scrollTop = resultPanel.scrollHeight;
}
function clearLog() { resultPanel.innerHTML = ''; }
// 定义三个模拟库的代码
const mathLibCode = `
var MathLib = {
fibonacci: function(n) {
if (n <= 1) return n;
var a = 0, b = 1;
for (var i = 2; i <= n; i++) { var t = a + b; a = b; b = t; }
return b;
},
isPrime: function(num) {
if (num < 2) return false;
for (var i = 2; i <= Math.sqrt(num); i++) { if (num % i === 0) return false; }
return true;
},
factorial: function(n) {
var r = 1;
for (var i = 2; i <= n; i++) r *= i;
return r;
},
gcd: function(a, b) { return b === 0 ? a : MathLib.gcd(b, a % b); },
lcm: function(a, b) { return Math.abs(a * b) / MathLib.gcd(a, b); }
};
`;
const stringLibCode = `
var StringLib = {
reverse: function(s) { return s.split('').reverse().join(''); },
toCamelCase: function(s) {
return s.toLowerCase().replace(/[-_](.)/g, function(_, c) { return c.toUpperCase(); });
},
wordCount: function(s) {
var words = s.trim().split(/\\s+/).filter(function(w){return w.length>0;});
var freq = {};
words.forEach(function(w){freq[w]=(freq[w]||0)+1;});
return { totalWords: words.length, frequency: freq };
},
toBase64: function(s) { return btoa(unescape(encodeURIComponent(s))); },
fromBase64: function(s) { return decodeURIComponent(escape(atob(s))); }
};
`;
const dataLibCode = `
var DataLib = {
unique: function(arr) {
var seen = {};
return arr.filter(function(item) {
var k = typeof item + ':' + item;
return seen[k] ? false : (seen[k] = true);
});
},
groupBy: function(arr, keyFn) {
var groups = {};
arr.forEach(function(item) {
var k = typeof keyFn === 'function' ? keyFn(item) : item[keyFn];
(groups[k] = groups[k] || []).push(item);
});
return groups;
},
stats: function(arr) {
if (arr.length === 0) return {};
var sum = arr.reduce(function(a,b){return a+b;},0);
var sorted = arr.slice().sort(function(a,b){return a-b;});
var mid = Math.floor(sorted.length / 2);
return {
count: arr.length,
sum: sum,
avg: sum / arr.length,
min: sorted[0],
max: sorted[sorted.length-1],
median: sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid-1]+sorted[mid])/2
};
},
chunk: function(arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) chunks.push(arr.slice(i, i+size));
return chunks;
}
};
`;
function runTest() {
const func = document.getElementById('funcSelect').value;
const param = document.getElementById('funcParam').value.trim();
const runBtn = document.getElementById('runBtn');
clearLog();
runBtn.disabled = true;
log(`开始执行: ${func}(${param})`, 'info');
log(`正在通过 importScripts 加载依赖库...`, 'info');
// 更新库状态显示
document.getElementById('status-math').textContent = '加载中...';
document.getElementById('status-string').textContent = '加载中...';
document.getElementById('status-data').textContent = '加载中...';
// 构建完整的 Worker 代码(包含三个库 + 主逻辑)
const workerCode = `
// ====== 通过 importScripts 加载三个模拟库 ======
` +
`importScripts(` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(mathLibCode))) + `',` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(stringLibCode))) + `',` +
`'data:text/javascript;base64,` + btoa(unescape(encodeURIComponent(dataLibCode))) + `'` +
`);
// 库加载完成,通知主线程
self.postMessage({ type: 'loaded', libs: ['MathLib', 'StringLib', 'DataLib'] });
self.onmessage = function(e) {
var data = e.data;
var funcName = data.func;
var param = data.param;
var result;
try {
switch (funcName) {
case 'fibonacci':
result = MathLib.fibonacci(parseInt(param));
break;
case 'isPrime':
result = MathLib.isPrime(parseInt(param));
break;
case 'factorial':
result = MathLib.factorial(parseInt(param));
break;
case 'reverseString':
result = StringLib.reverse(param);
break;
case 'wordCount':
result = StringLib.wordCount(param);
break;
case 'toCamelCase':
result = StringLib.toCamelCase(param);
break;
case 'unique':
result = DataLib.unique(JSON.parse(param));
break;
case 'groupBy':
result = DataLib.groupBy(JSON.parse(param), data.key || 'type');
break;
case 'stats':
result = DataLib.stats(JSON.parse(param));
break;
default:
throw new Error('未知函数: ' + funcName);
}
self.postMessage({
type: 'result',
func: funcName,
param: param,
result: result,
resultType: typeof result
});
} catch(err) {
self.postMessage({ type: 'error', message: err.message, func: funcName });
}
};
self.onerror = function(e) {
self.postMessage({ type: 'error', message: e.message });
};
`;
try {
const blob = new Blob([workerCode], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = function(e) {
const d = e.data;
if (d.type === 'loaded') {
log(`✅ 库加载完成: ${d.libs.join(', ')}`, 'ok');
document.getElementById('status-math').className = 'lib-status loaded';
document.getElementById('status-math').textContent = '已加载 ✓';
document.getElementById('status-string').className = 'lib-status loaded';
document.getElementById('status-string').textContent = '已加载 ✓';
document.getElementById('status-data').className = 'lib-status loaded';
document.getElementById('status-data').textContent = '已加载 ✓';
// 发送任务
let parsedParam = param;
if (['unique', 'groupBy', 'stats'].includes(func)) {
try { parsedParam = param; } catch(e) {}
}
worker.postMessage({ func: func, param: param });
log(`发送任务: ${func}(${param})`, 'info');
} else if (d.type === 'result') {
const resultStr = typeof d.result === 'object'
? JSON.stringify(d.result, null, 2).substring(0, 500)
: d.result.toLocaleString ? d.result.toLocaleString() : d.result;
log(`━━━ 执行结果 ━━━`, 'highlight');
log(`函数: ${d.func}(${d.param})`, 'info');
log(`返回类型: ${d.resultType}`, 'info');
log(`结果: ${resultStr}`, 'ok');
log(`━━━━━━━━━━━━━━━`, 'highlight');
runBtn.disabled = false;
worker.terminate();
} else if (d.type === 'error') {
log(`❌ 错误 [${d.func}]: ${d.message}`, 'err');
runBtn.disabled = false;
worker.terminate();
}
};
worker.onerror = function(e) {
log(`❌ Worker 错误: ${e.message} (${e.filename}:${e.lineno})`, 'err');
runBtn.disabled = false;
e.preventDefault();
};
} catch (err) {
log(`❌ 创建 Worker 失败: ${err.message}`, 'err');
runBtn.disabled = false;
}
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【10】Worker 消息传递性能对比</title>
<!--
来源: HTML5基础知识/15-WebWorkers处理线程.md
知识点: Structured Clone vs Transferable vs SharedArrayBuffer 性能对比
-->
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px; background: #121212; color: #e0e0e0;
min-height: 100vh;
}
.container { max-width: 1080px; margin: 0 auto; }
.header {
text-align: center; padding: 24px; background: linear-gradient(135deg, #e040fb, #7c4dff);
color: white; border-radius: 12px; margin-bottom: 24px;
}
.header h1 { font-size: 22px; margin-bottom: 6px; }
.header p { opacity: 0.9; font-size: 14px; }
.card {
background: #1e1e1e; border: 1px solid #333; border-radius: 10px;
padding: 24px; margin-bottom: 20px;
}
.card-title {
font-size: 15px; font-weight: 600; color: #e040fb;
border-left: 3px solid #e040fb; padding-left: 10px; margin-bottom: 16px;
}
.info-banner {
background: rgba(224,64,251,0.1); border: 1px solid rgba(224,64,251,0.25);
border-radius: 6px; padding: 12px 16px; font-size: 13px; line-height: 1.6;
color: #ce93d8; margin-bottom: 16px;
}
.method-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
@media (max-width: 800px) { .method-cards { grid-template-columns: 1fr; } }
.method-card {
background: #121212; border: 2px solid #333; border-radius: 10px;
padding: 18px; text-align: center; transition: all 0.3s;
}
.method-card.active { border-color: #e040fb; box-shadow: 0 0 20px rgba(224,64,251,0.15); }
.method-icon { font-size: 28px; margin-bottom: 8px; }
.method-name { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.method-desc { font-size: 11px; color: #888; line-height: 1.5; margin-bottom: 12px; }
.method-result { font-family: monospace; font-size: 22px; font-weight: 700; color: #e040fb; }
.method-unit { font-size: 11px; color: #666; }
.method-bar-container {
height: 6px; background: #333; border-radius: 3px; margin-top: 10px; overflow: hidden;
}
.method-bar { height: 100%; border-radius: 3px; transition: width 0.5s ease; width: 0%; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; align-items: center; }
label { font-size: 13px; color: #aaa; }
select, input[type="number"] {
padding: 8px 12px; background: #121212; border: 1px solid #444;
color: #fff; border-radius: 6px; font-size: 13px;
}
select:focus, input:focus { outline: none; border-color: #e040fb; }
.btn {
padding: 10px 22px; border: none; border-radius: 6px; cursor: pointer;
font-size: 13px; font-weight: 600; transition: all 0.2s;
}
.btn-primary { background: linear-gradient(135deg, #e040fb, #7c4dff); color: white; }
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(224,64,251,0.3); }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }
/* 对比表格 */
.compare-table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 16px; }
.compare-table th, .compare-table td {
padding: 12px 14px; text-align: left; border-bottom: 1px solid #333;
}
.compare-table th { background: #1a1a2e; color: #aaa; font-weight: 600; }
.compare-table tr:hover td { background: #1a1a2e; }
.chart-container {
background: #121212; border-radius: 8px; padding: 20px; margin-top: 16px;
}
.bar-chart { display: flex; align-items: flex-end; gap: 30px; height: 220px; padding: 0 20px; }
.chart-col { display: flex; flex-direction: column; align-items: center; flex: 1; }
.chart-bar-wrapper {
width: 60px; height: 180px; background: #222; border-radius: 6px 6px 0 0;
display: flex; flex-direction: column-reverse; align-items: center;
position: relative; overflow: hidden;
}
.chart-bar {
width: 100%; border-radius: 4px 4px 0 0; transition: height 0.6s ease;
min-height: 2px;
}
.chart-bar.clone { background: linear-gradient(to top, #ef5350, #e53935); }
.chart-bar.transfer { background: linear-gradient(to top, #42a5f5, #1e88e5); }
.chart-bar.shared { background: linear-gradient(to top, #66bb6a, #43a047); }
.chart-value { font-size: 12px; font-weight: 700; color: #fff; margin-bottom: 4px; z-index: 1; }
.chart-label { font-size: 12px; color: #888; margin-top: 8px; font-weight: 600; }
.detail-log {
background: #0a0a0a; border-radius: 6px; padding: 14px;
font-family: monospace; font-size: 11px; max-height: 200px;
overflow-y: auto; color: #999; line-height: 1.7; margin-top: 16px;
}
.log-ok { color: #66bb6a; }
.log-info { color: #42a5f5; }
.log-warn { color: #ffa726; }
.log-err { color: #ef5350; }
.compat-note {
background: rgba(255,167,38,0.1); border: 1px solid rgba(255,167,38,0.25);
border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #ffa726;
margin-top: 16px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Worker 消息传递性能对比</h1>
<p>Structured Clone vs Transferable vs SharedArrayBuffer — 数据传输方式基准测试</p>
</div>
<div class="card">
<div class="card-title">🧪 基准测试</div>
<div class="info-banner">
💡 <strong>三种数据传递方式:</strong><br>
• <strong>Structured Clone</strong> — 默认方式,数据被复制(大对象开销大)<br>
• <strong>Transferable Objects</strong> — 所有权转移,零拷贝(原变量不可再用)<br>
• <strong>SharedArrayBuffer</strong> — 共享内存,零拷贝且双方可读写(需要特殊头)
</div>
<div class="controls">
<label>数据大小 (KB):</label>
<select id="dataSize">
<option value="1">1 KB</option>
<option value="10">10 KB</option>
<option value="100" selected>100 KB</option>
<option value="1024">1 MB</option>
<option value="10240">10 MB</option>
</select>
<label>往返次数:</label>
<input type="number" id="roundTrips" value="100" min="10" max="1000" step="10" />
<button class="btn btn-primary" id="benchBtn" onclick="runBenchmark()">🚀 运行基准测试</button>
</div>
<div class="method-cards">
<div class="method-card" id="cardClone">
<div class="method-icon">📋</div>
<div class="method-name">Structured Clone</div>
<div class="method-desc">默认方式,完整复制数据。安全但大对象慢。</div>
<div class="method-result" id="resClone">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barClone" style="background:#ef5350;"></div></div>
</div>
<div class="method-card" id="cardTransfer">
<div class="method-icon">🚀</div>
<div class="method-name">Transferable</div>
<div class="method-desc">所有权转移,零拷贝。最快但原数据失效。</div>
<div class="method-result" id="resTransfer">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barTransfer" style="background:#42a5f5;"></div></div>
</div>
<div class="method-card" id="cardShared">
<div class="method-icon">🔗</div>
<div class="method-name">SharedArrayBuffer</div>
<div class="method-desc">共享内存区域,零拷贝可读写。需 COOP/COEP 头。</div>
<div class="method-result" id="resShared">--</div>
<div class="method-unit">ms (总耗时)</div>
<div class="method-bar-container"><div class="method-bar" id="barShared" style="background:#66bb6a;"></div></div>
</div>
</div>
<!-- 柱状图 -->
<div class="chart-container">
<div class="bar-chart" id="barChart">
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar clone" id="chartBarClone" style="height:0%"></div>
<div class="chart-value" id="chartValClone">--</div>
</div>
<div class="chart-label">Clone</div>
</div>
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar transfer" id="chartBarTransfer" style="height:0%"></div>
<div class="chart-value" id="chartValTransfer">--</div>
</div>
<div class="chart-label">Transferable</div>
</div>
<div class="chart-col">
<div class="chart-bar-wrapper">
<div class="chart-bar shared" id="chartBarShared" style="height:0%"></div>
<div class="chart-value" id="chartValShared">--</div>
</div>
<div class="chart-label">SharedAB</div>
</div>
</div>
</div>
<div class="detail-log" id="detailLog">
<div class="log-info">[系统] 点击 "运行基准测试" 开始性能对比...</div>
</div>
</div>
<!-- 对比表格 -->
<div class="card">
<div class="card-title">📖 详细对比</div>
<table class="compare-table">
<thead>
<tr>
<th>特性</th>
<th>Structured Clone</th>
<th>Transferable</th>
<th>SharedArrayBuffer</th>
</tr>
</thead>
<tbody>
<tr>
<td>拷贝开销</td>
<td>完整深拷贝(高开销)</td>
<td>零拷贝(转移所有权)</td>
<td>零拷贝(共享内存)</td>
</tr>
<tr>
<td>原数据可用性</td>
<td>✅ 可用(副本独立)</td>
<td>❌ 不可用(已被转移)</td>
<td>✅ 双方均可访问</td>
</tr>
<tr>
<td>数据大小限制</td>
<td>受内存限制</td>
<td>受内存限制</td>
<td>受内存限制</td>
</tr>
<tr>
<td>支持的类型</td>
<td>大部分 JS 对象</td>
<td>ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas</td>
<td>仅 ArrayBuffer</td>
</tr>
<tr>
<td>安全性</td>
<td>✅ 高(隔离)</td>
<td>✅ 高(唯一所有权)</td>
<td>⚠️ 需防止竞态条件</td>
</tr>
<tr>
<td>特殊要求</td>
<td>无</td>
<td>无</td>
<td>COOP/COEP 安全头</td>
</tr>
<tr>
<td>典型场景</td>
<td>普通消息传递</td>
<td>大数据量传输</td>
<td>高频实时数据共享</td>
</tr>
</tbody>
</table>
<div class="compat-note">
⚠️ <strong>SharedArrayBuffer 兼容性:</strong>由于 Spectre 漏洞安全问题,现代浏览器要求页面设置
<code>Cross-Origin-Opener-Policy: same-origin</code> 和 <code>Cross-Origin-Embedder-Policy: require-corp</code>
安全头才能启用 SharedArrayBuffer。本地 file:// 协议通常不可用。
</div>
</div>
</div>
<script>
const detailLog = document.getElementById('detailLog');
function log(msg, cls = '') {
const div = document.createElement('div');
div.className = cls || '';
const t = new Date().toTimeString().substring(0, 8);
div.textContent = `[${t}] ${msg}`;
detailLog.appendChild(div);
detailLog.scrollTop = detailLog.scrollHeight;
}
async function runBenchmark() {
const btn = document.getElementById('benchBtn');
btn.disabled = true;
const sizeKB = parseInt(document.getElementById('dataSize').value);
const rounds = parseInt(document.getElementById('roundTrips').value) || 100;
const sizeBytes = sizeKB * 1024;
detailLog.innerHTML = '';
log(`=== 基准测试开始 ===`);
log(`数据大小: ${sizeKB} KB (${sizeBytes} bytes) | 往返次数: ${rounds}`);
// 重置卡片
['Clone', 'Transfer', 'Shared'].forEach(m => {
document.getElementById(`res${m}`).textContent = '--';
document.getElementById(`bar${m}`).style.width = '0%';
document.getElementById(`chartBar${m}`).style.height = '0%';
document.getElementById(`chartVal${m}`).textContent = '--';
document.getElementById(`card${m.replace('Transfer','Transfer').replace('Shared','Shared')}`).classList.remove('active');
});
// 创建测试数据
const testData = new Uint8Array(sizeBytes);
for (let i = 0; i < sizeBytes; i++) testData[i] = i & 0xff;
const results = {};
// ====== 测试 1: Structured Clone ======
log('', '');
log('--- 测试 1: Structured Clone ---', 'info');
document.getElementById('cardClone').classList.add('active');
results.clone = await benchmarkMethod('clone', testData, sizeBytes, rounds, () => {
// 每次 copy 数据
const arr = new Uint8Array(testData);
return { buffer: arr.buffer, method: 'clone' };
}, (w, data) => {
w.postMessage(data);
});
// ====== 测试 2: Transferable ======
log('', '');
log('--- 测试 2: Transferable ---', 'info');
document.getElementById('cardTransfer').classList.add('active');
results.transfer = await benchmarkMethod('transfer', testData, sizeBytes, rounds, () => {
const ab = new ArrayBuffer(sizeBytes);
new Uint8Array(ab).set(testData);
return { buffer: ab };
}, (w, data) => {
w.postMessage(data, [data.buffer]); // Transfer!
});
// ====== 测试 3: SharedArrayBuffer ======
log('', '');
log('--- 测试 3: SharedArrayBuffer ---', 'info');
document.getElementById('cardShared').classList.add('active');
if (typeof SharedArrayBuffer !== 'undefined') {
results.shared = await benchmarkMethod('shared', testData, sizeBytes, rounds, () => {
try {
const sab = new SharedArrayBuffer(sizeBytes);
new Uint8Array(sab).set(testData);
return { buffer: sab };
} catch(e) {
return null; // 可能因为 COOP/COEP 不可用
}
}, (w, data) => {
if (!data || !data.buffer) throw new Error('SAB unavailable');
w.postMessage(data); // SAB 自动共享,不需要 transfer list
});
} else {
log('⚠️ SharedArrayBuffer 不可用(可能缺少 COOP/COEP 安全头)', 'warn');
results.shared = null;
}
// ====== 显示结果 ======
displayResults(results);
log('', '');
log(`=== 测试完成 ===`, 'log-ok');
btn.disabled = false;
}
function benchmarkMethod(name, originalData, sizeBytes, rounds, prepareFn, sendFn) {
return new Promise((resolve) => {
const code = `
self.onmessage = function(e) {
if (e.data.method === 'shared') {
// SharedArrayBuffer: 直接读取并返回确认
self.postMessage({ received: true, firstByte: new Uint8Array(e.data.buffer)[0] });
} else {
// Clone/Transfer: 返回数据信息
self.postMessage({
received: true,
byteLength: e.data.buffer.byteLength,
firstByte: new Uint8Array(e.data.buffer)[0]
});
}
};
`;
const blob = new Blob([code], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
let completed = 0;
const startTime = performance.now();
worker.onmessage = function() {
completed++;
if (completed >= rounds) {
const elapsed = performance.now() - startTime;
worker.terminate();
resolve(elapsed);
} else {
try {
const data = prepareFn();
if (!data) throw new Error('prepare failed');
sendFn(worker, data);
} catch(err) {
// 出错时提前结束
const elapsed = performance.now() - startTime;
worker.terminate();
resolve(elapsed);
}
}
};
worker.onerror = function(e) {
log(`[${name}] Worker 错误: ${e.message}`, 'log-err');
resolve(performance.now() - startTime);
e.preventDefault();
};
// 发送第一个任务
try {
const data = prepareFn();
if (!data) throw new Error('prepare failed');
sendFn(worker, data);
} catch(err) {
log(`[${name}] 准备失败: ${err.message},跳过`, 'log-warn');
resolve(null);
}
});
}
function displayResults(results) {
const times = [
{ key: 'clone', val: results.clone },
{ key: 'transfer', val: results.transfer },
{ key: 'shared', val: results.shared }
];
const validTimes = times.filter(t => t.val !== null && t.val !== undefined);
if (validTimes.length === 0) return;
const maxTime = Math.max(...validTimes.map(t => t.val));
times.forEach(t => {
const el = t.key.charAt(0).toUpperCase() + t.key.slice(1);
const capKey = el === 'Clone' ? 'Clone' : el === 'Transfer' ? 'Transfer' : 'Shared';
if (t.val === null || t.val === undefined) {
document.getElementById(`res${capKey}`).textContent = 'N/A';
log(`[${capKey}] 不可用`, 'log-warn');
return;
}
const timeMs = t.val.toFixed(1);
const pct = maxTime > 0 ? (t.val / maxTime * 100) : 0;
document.getElementById(`res${capKey}`).textContent = `${timeMs}`;
document.getElementById(`bar${capKey}`).style.width = `${pct}%`;
document.getElementById(`chartBar${capKey}`).style.height = `${Math.max(pct, 2)}%`;
document.getElementById(`chartVal${capKey}`).textContent = `${timeMs}`;
log(`[${capKey}] 总耗时: ${timeMs} ms | 平均: ${(t.val / (parseInt(document.getElementById('roundTrips').value)||100)).toFixed(2)} ms/次`,
'log-ok');
});
// 计算加速比
if (results.clone && results.transfer) {
const speedup = (results.clone / results.transfer).toFixed(1);
log(``, '');
log(`🚀 Transferable 比 Structured Clone 快 ${speedup}x`, 'log-info');
}
if (results.clone && results.shared) {
const speedup2 = (results.clone / results.shared).toFixed(1);
log(`🔗 SharedArrayBuffer 比 Structured Clone 快 ${speedup2}x`, 'log-info');
}
}
</script>
</body>
</html>