{T}

Web Workers 处理线程

Web Workers 是 HTML5 引入的一项重要技术,允许在浏览器后台运行 JavaScript 脚本,而不会阻塞主线程的用户界面交互。这使得开发者能够执行计算密集型任务而不影响页面的响应速度。

概述

Web Workers 的主要类型

  1. 专用 Worker (Dedicated Worker):仅服务于创建它的页面,是最常用的类型
  2. 共享 Worker (Shared Worker):可被同一浏览器上下文中的多个页面共享,用于跨页面通信
  3. Service Worker:特殊的 Worker,用于拦截和处理网络请求,实现离线缓存、推送通知等功能

核心概念

  1. 独立线程:Web Workers 在独立的线程中运行,与主线程并行执行
  2. 消息通信:通过 postMessage()onmessage 事件进行线程间通信
  3. 无 DOM 访问:Worker 不能直接访问 DOM,只能进行计算和数据处理
  4. 同源限制:Worker 脚本必须与主线程同源(或通过 CORS 配置)
  5. 生命周期:由主线程创建和控制,可通过 terminate() 方法终止
  6. 数据传递:使用结构化克隆算法传递数据,支持大多数 JavaScript 对象类型

浏览器兼容性

浏览器专用 Worker共享 WorkerService 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 单线程模型的限制。

plaintext
┌─────────────────────────────────────────────────────────────┐
│                         浏览器进程                           │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────────┐      ┌──────────────────┐            │
│  │   主线程 (UI)     │      │  Worker 线程 1   │            │
│  │  ┌────────────┐  │      │  ┌────────────┐  │            │
│  │  │ DOM 操作   │  │      │  │ 计算任务   │  │            │
│  │  │ 事件处理   │◄─┼──────┼─►│ 数据处理   │  │            │
│  │  │ UI 渲染    │  │      │  │ 网络请求   │  │            │
│  │  └────────────┘  │      │  └────────────┘  │            │
│  └──────────────────┘      └──────────────────┘            │
│         │                           │                       │
│         │ postMessage               │ postMessage           │
│         ▼                           ▼                       │
│  ┌──────────────────────────────────────────────────┐      │
│  │         消息队列 (MessageChannel)                │      │
│  └──────────────────────────────────────────────────┘      │
│                                     │                       │
│                          ┌──────────────────┐              │
│                          │  Worker 线程 2   │              │
│                          │  ┌────────────┐  │              │
│                          │  │ 图像处理   │  │              │
│                          │  │ 文件解析   │  │              │
│                          │  └────────────┘  │              │
│                          └──────────────────┘              │
└─────────────────────────────────────────────────────────────┘

通信架构

Worker 线程与主线程之间采用消息传递机制进行通信,这是一种异步、线程安全的通信方式:

消息传递流程

plaintext
主线程                          Worker 线程
  │                               │
  │ 1. postMessage(data)          │
  ├──────────────────────────────►│
  │                               │ 2. 序列化数据
  │                               │    (结构化克隆)
  │                               │
  │                               │ 3. 执行计算任务
  │                               │
  │ 4. 反序列化数据               │
  │◄──────────────────────────────┤
  │ 5. 触发 onmessage 事件        │ 6. postMessage(result)
  │                               │

关键特性

  1. 数据隔离: 每个线程有独立的内存空间,数据传递时进行复制或转移
  2. 异步通信: 消息传递是非阻塞的,不会影响线程执行
  3. 类型支持: 使用结构化克隆算法,支持大多数 JavaScript 数据类型
  4. 错误传播: Worker 中的错误可以通过 onerror 事件传递到主线程

线程生命周期

plaintext
创建 ──► 运行 ──► 空闲 ──► 终止
 │       │       │        │
 │       │       │        └─ worker.terminate() / self.close()
 │       │       │
 │       │       └─ 任务完成,等待新消息
 │       │
 │       └─ 接收消息,执行任务

 └─ new Worker(scriptURL)

资源占用

资源类型主线程Worker 线程
DOM 访问✅ 完全访问❌ 无法访问
Window 对象✅ 可用❌ 不可用
localStorage✅ 可用❌ 不可用
IndexedDB✅ 可用✅ 可用
Fetch API✅ 可用✅ 可用
WebSocket✅ 可用✅ 可用
Canvas✅ 可用✅ OffscreenCanvas
内存空间独立独立

Worker 类型选型指南

Web Workers 生态中存在多种 Worker 类型,每种都有其特定的使用场景和约束。以下是完整的分类与决策流程:

图表渲染中…

各类型详细对比

特性Dedicated WorkerShared WorkerService WorkerOffscreenCanvas WorkerWorkletWasm Worker
生命周期随页面销毁所有页面关闭后销毁注册后持久运行随页面销毁浏览器管理随页面销毁
通信方式postMessageport.postMessagefetch 事件 / postMessageBitmap / 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>
html
<!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 CloneTransferableSharedArrayBufferMessagePort
原理深拷贝数据到新内存所有权转移,零拷贝共享同一块内存双向通信管道
速度中等(需复制)最快(指针移动)最快(无传输)快(轻量级)
原数据可用性✅ 仍可使用❌ 变为空/不可用✅ 双方均可读写N/A
数据大小限制数百 MB无限制需配置 COOP/COEP无限制
线程安全性天然安全天然安全需 Atomics 保证天然安全
适用数据任意可克隆对象ArrayBuffer 等SharedArrayBuffer任意可克隆对象
浏览器要求全部支持全部支持需跨域隔离头全部支持
使用建议
javascript
// 场景 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 生命周期状态机

图表渲染中…

各状态特征说明

状态触发条件可否恢复资源占用
Creatingnew Worker() 调用-已分配但未初始化
Loading开始下载脚本-正在加载脚本文件
Running脚本加载并执行成功-活跃状态,全量资源
Idle无待处理消息自动转 Running保持活跃,监听中
Terminatedterminate() 强制终止❌ 不可恢复已释放
Closedself.close() 优雅退出❌ 不可恢复已释放
Error加载/执行出错❌ 不可恢复部分已释放
注意事项
  • terminate() 是立即生效的暴力终止,正在执行的代码会被打断,pending 的异步操作会被取消
  • self.close() 是优雅退出,Worker 可以在关闭前做清理工作并发送最后一条消息
  • 一旦进入 Terminated 或 Closed 状态,该 Worker 实例无法重新使用,必须创建新的

专用 Worker

基本使用示例

image-20250528020617388

复杂数据传递示例

Web Workers 使用结构化克隆算法(Structured Clone Algorithm)来传递数据。这个算法支持大多数 JavaScript 对象类型,但有一些限制。

支持的数据类型:

  • ✅ 基本类型:StringNumberBooleannullundefined
  • ✅ 对象:ObjectArrayDateRegExp
  • ✅ 集合类型:MapSetArrayBufferTypedArray
  • BlobFileImageData
  • FunctionErrorDOM 元素Symbol

Worker 构造函数参数详解

创建 Worker 实例时,可以传递配置选项来控制 Worker 的行为:

javascript
const worker = new Worker(scriptURL, options)

参数说明

scriptURL (必需)

  • Worker 脚本的 URL 地址
  • 必须与主线程同源,或配置 CORS
  • 可以是相对路径或绝对路径

options (可选)

属性类型默认值说明
typestring"classic"Worker 类型:"classic""module"
credentialsstring"omit"凭证模式:"omit""same-origin""include"
namestring""Worker 名称,用于调试识别

使用示例

Inline Worker 与 Blob Worker

除了从外部文件加载 Worker,还可以使用 Blob URL 或 Data URL 创建内联 Worker。这种方式适用于:

  • 单文件应用
  • 动态生成 Worker 代码
  • 避免额外的网络请求

Blob Worker

Data URL Worker

javascript
// 使用 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简单直接有长度限制,编码开销极简单场景

最佳实践

javascript
// ✅ 推荐: 使用外部文件 + 模块模式
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 来提高性能,避免数据复制

主线程代码:

javascript
const worker = new Worker("worker.js")
 
// 创建大型 ArrayBuffer
const largeBuffer = new ArrayBuffer(10000000) // 10MB
 
// 转移所有权(不是复制)
worker.postMessage({ buffer: largeBuffer }, [largeBuffer])
 
// 注意:转移后主线程不能再使用 largeBuffer

Worker 线程代码:

javascript
self.onmessage = function (event) {
  const buffer = event.data.buffer
  // 现在拥有 buffer 的所有权
  // 可以进行处理...
}

2. 异步 API 在 Worker 中的使用

Worker 中可以使用大多数异步 API,如 Fetch API、setTimeout 等

Worker 代码示例:

javascript
// 使用 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 代码:

javascript
// 动态导入模块
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:

javascript
const worker = new Worker("worker.mjs", { type: "module" })

Worker 内使用 ES Module:

javascript
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 示例:

javascript
const data = await fetch("/api/config").then((r) => r.json())
 
self.onmessage = (event) => {
  console.log("配置已加载:", data)
  self.postMessage({ config: data, input: event.data })
}
TIP

ES Module Workers 是现代 Web 应用的推荐方式,但需要浏览器支持(Chrome 80+、Firefox 114+、Safari 15+)。不支持的环境会回退到 Classic Worker。

5. SharedArrayBuffer 与 Atomics

SharedArrayBuffer 允许主线程和 Worker 共享同一块内存,实现零拷贝的高效数据交换。Atomics 提供原子操作,确保共享内存的线程安全。

基本原理:

plaintext
主线程                    Worker 线程
  │                          │
  ├── SharedArrayBuffer ─────┤  ← 同一块内存
  │                          │
  ├── Atomics.store()        ├── Atomics.load()
  └── Atomics.wait()         └── Atomics.notify()

创建共享内存:

javascript
const sharedBuffer = new SharedArrayBuffer(4)
const sharedArray = new Int32Array(sharedBuffer)
 
const worker = new Worker("worker.js")
worker.postMessage({ buffer: sharedBuffer })

Worker 中使用共享内存:

javascript
self.onmessage = (event) => {
  const sharedArray = new Int32Array(event.data.buffer)
 
  Atomics.store(sharedArray, 0, 42)
  Atomics.notify(sharedArray, 0)
}

主线程等待 Worker 更新:

javascript
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 并行计算

javascript
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} 完成`)
  }
})
WARNING

由于 Spectre 漏洞的安全影响,SharedArrayBuffer 要求页面使用跨域隔离(Cross-Origin Isolation):

  • 服务器须返回 Cross-Origin-Opener-Policy: same-origin
  • 服务器须返回 Cross-Origin-Embedder-Policy: require-corp

未配置这些响应头时,SharedArrayBuffer 在部分浏览器中不可用。可通过 self.crossOriginIsolated 检测:

javascript
if (self.crossOriginIsolated) {
  const sab = new SharedArrayBuffer(1024)
} else {
  console.warn("页面未跨域隔离,SharedArrayBuffer 不可用")
}

6. MessageChannel(消息通道)

MessageChannel 允许在两个 Worker 之间或 Worker 与主线程之间建立直接通信通道。

7. Worker 池(Worker Pool)

对于需要处理大量任务的场景,可以使用 Worker 池来管理多个 Worker 实例,提高并发处理能力。

javascript
// 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 的所有执行,释放关联的资源,并终止所有未完成的异步操作

javascript
// 创建 Worker 实例
const worker = new Worker("worker.js")
 
// ...执行任务...
 
// 终止 Worker
worker.terminate()

关键特性:

  1. 即时生效:调用后 Worker 会立即停止执行,不会等待当前任务完成
  2. 单向操作:终止后无法恢复,需重新创建实例
  3. 资源释放:自动清理 Worker 占用的内存和线程资源
  4. 通信中断:终止后所有 pending 的消息传递将被丢弃

最佳实践:

  • 在确定不再需要 Worker 时(如组件卸载、页面关闭前)及时调用
  • 对于长时间运行的 Worker,建议实现心跳检测机制
  • 在 Worker 内部可以调用 self.close() 方法让 Worker 进行优雅退出
javascript
// Worker 内部优雅退出
self.onmessage = function (event) {
  if (event.data === "terminate") {
    // 清理资源
    // ...
    self.close() // Worker 自己关闭
  }
}

性能优化建议

  1. 合理使用 Transferable Objects

    • 对于大型 ArrayBufferImageData 等,使用 transferable objects 避免复制
    • 转移后原对象将无法使用,需要重新创建
  2. 避免频繁创建和销毁 Worker

    • 对于需要多次使用的 Worker,应该复用而不是每次创建新的
    • 使用 Worker 池管理多个 Worker 实例
  3. 批量处理数据

    • 将多个小任务合并为一个大任务,减少消息传递次数
    • 使用批处理模式处理数组数据
  4. 控制 Worker 数量

    • 使用 navigator.hardwareConcurrency 获取 CPU 核心数
    • Worker 数量不应超过 CPU 核心数太多
javascript
// 获取 CPU 核心数
const cpuCores = navigator.hardwareConcurrency || 4
console.log("CPU 核心数:", cpuCores)
 
// 根据核心数创建 Worker 池
const poolSize = Math.min(cpuCores, 8) // 最多 8 个
  1. 使用进度反馈
    • 对于长时间运行的任务,定期发送进度更新
    • 避免主线程长时间等待
javascript
// 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
      })
    }
  }
}

调试技巧

  1. 使用 Chrome DevTools

    • 在 Sources 面板中可以找到 Worker 脚本
    • 可以在 Worker 脚本中设置断点
    • 在 Console 面板中可以看到 Worker 的 console.log 输出
  2. 添加调试标识

    • 在 Worker 消息中添加类型标识,便于区分不同消息
    • 使用时间戳追踪消息传递
javascript
// Worker 中
self.postMessage({
  type: "result",
  data: result,
  timestamp: Date.now(),
  workerId: "worker-1"
})
  1. 错误追踪
    • 使用 try-catch 包装 Worker 代码
    • 记录详细的错误堆栈信息
javascript
self.onmessage = function (event) {
  try {
    // 处理逻辑
  } catch (error) {
    self.postMessage({
      type: "error",
      message: error.message,
      stack: error.stack,
      data: event.data
    })
  }
}
  1. 性能监控
    • 使用 performance.now() 测量 Worker 执行时间
    • 监控消息传递的延迟
javascript
// 主线程
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> 标签)

示例:

javascript
// 导入单个脚本
importScripts("utils.js")
 
// 导入多个脚本(按顺序执行)
importScripts("math-utils.js", "string-utils.js", "api-wrapper.js")

Web API 支持

Web Worker 支持大部分 Web API,但有一些限制:

API 类别支持情况说明
网络请求✅ 支持XMLHttpRequestfetch()(现代浏览器)
存储✅ 部分支持localStoragesessionStorage 不可用(因无 DOM 访问权限)<br>IndexedDB 可用
定时器✅ 支持setTimeout()setInterval()
WebSocket✅ 支持可使用 WebSocket API 进行实时通信
导航❌ 不支持无法访问 windowdocument 对象
File API✅ 支持FileReaderBlobFile
URL API✅ 支持URLURLSearchParams
Crypto API✅ 支持crypto.subtle(需要 HTTPS)

其他可用功能

  • 全局对象navigator(精简版)、consoleJSON
  • 核心函数eval()isNaN()isFinite()parseFloat()parseInt()
  • 错误处理try...catch 语句
  • 本地对象:可以创建和使用自定义对象、数组、Date 等
  • 数学计算Math 对象的所有方法
  • 正则表达式:完整支持

注意事项

  1. 同源策略:所有导入的脚本必须与 Worker 文件同源
  2. 同步加载importScripts() 是同步操作,会阻塞 Worker 执行
  3. 无 DOM 访问:Worker 无法访问 documentwindow 等 DOM 相关对象
  4. 存储限制:不能使用 localStorage,但可以使用 IndexedDB
  5. 脚本隔离:每个 Worker 有独立的全局作用域

Worker 事件类型

Worker 支持多种事件类型,用于处理不同的场景:

主线程事件

事件类型触发时机事件对象属性说明
message接收到 Worker 发送的消息data: 消息数据<br>origin: 消息来源<br>source: 消息源窗口<br>ports: MessagePort 数组最常用的事件,用于接收 Worker 的处理结果
errorWorker 执行出错message: 错误消息<br>filename: 错误文件名<br>lineno: 错误行号<br>colno: 错误列号Worker 内部的未捕获错误会触发此事件
messageerror消息反序列化失败data: 无法反序列化的数据当 Worker 发送的数据无法被结构化克隆时触发

Worker 线程事件

事件类型触发时机事件对象属性说明
message接收到主线程发送的消息data: 消息数据接收主线程传递的数据
errorWorker 内部发生错误message: 错误消息<br>filename: 错误文件名<br>lineno: 错误行号Worker 内部的错误处理
messageerror消息反序列化失败data: 无法反序列化的数据主线程发送的数据无法被结构化克隆时触发
installService Worker 安装时-仅 Service Worker
activateService Worker 激活时-仅 Service Worker
fetch拦截网络请求时request: Request 对象仅 Service Worker

SharedWorker 特有事件

事件类型触发时机事件对象属性说明
connect新连接建立时ports: MessagePort 数组当新的客户端连接到 Shared Worker 时触发

MessagePort 事件

事件类型触发时机说明
message接收到端口消息用于 MessageChannel 通信
messageerror消息反序列化失败端口接收的数据无法序列化时触发

事件监听方式

javascript
// 方式 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)

事件处理最佳实践

javascript
// 完整的事件处理示例
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 受同源策略约束,确保应用安全性:

  1. 脚本来源限制
    • Worker 脚本必须与主页面同源
    • 跨域 Worker 需要 CORS 头配置
    • 不能从 file:// 协议加载 Worker
javascript
// ✅ 正确: 同源 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'); // 在本地文件中会失败
  1. 跨域 Worker 配置

数据安全

敏感数据处理

javascript
// ❌ 危险: 直接传递敏感数据
// 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 攻击

javascript
// ❌ 危险: 动态拼接 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 的消息都应该进行验证:

javascript
// 消息验证模式
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 提供天然的安全隔离:

  1. 独立的执行环境

    • 无法访问主线程的变量
    • 无法访问 DOM
    • 无法访问 localStoragesessionStorage
  2. 受限的 API 访问

    • 只能使用允许的 Web API
    • 不能执行某些敏感操作
  3. 错误隔离

    • Worker 中的错误不会直接导致主线程崩溃
    • 需要显式处理错误传播

最佳安全实践

javascript
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 脚本,这是最简洁的方式:

javascript
// 导入 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 类型:

javascript
// 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 行为进行更精细的控制:

javascript
// 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() 构造函数:

javascript
// 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 或需要更细粒度控制的场景:

javascript
// 安装: 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 导出
          }
        }
      }
    ]
  }
}
javascript
// 源码中使用
import MyWorker from './my.worker.js'
 
const worker = new MyWorker()
worker.postMessage('hello')

Comlink 是 Google Chrome Labs 出品的库,可以让 Worker 通信像调用本地函数一样自然:

javascript
// 安装: npm install comlink comlink-loader
 
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: ['comlink-loader']
      }
    ]
  }
}
javascript
// 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)
javascript
// 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

构建工具对比总结

特性ViteWebpack 5worker-loadercomlink-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 脚本

  1. 打开 DevTools → Sources 面板
  2. 左侧文件树顶部找到 Page 分组下展开 WorkersThreads
  3. 每个 Worker 以独立线程形式列出,点击即可查看源码
  4. 支持设置断点、单步执行、查看变量——与主线程调试体验一致
plaintext
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
└── Snippets

Console 面板的上下文切换

Console 面板默认显示主线程日志。要查看 Worker 的 console.log 输出:

  • Console 面板左上角的下拉框中选择目标 Context
    • top — 主线程
    • worker.js:1 — Worker 线程
    • shared-worker.js:1 — Shared Worker 线程
javascript
// 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 性能

  1. 打开 DevTools → Performance 面板
  2. 点击 Record 按钮(或按 Ctrl+E
  3. 执行触发 Worker 任务的交互操作
  4. 点击 Stop 结束录制
  5. 在时间轴中找到 Worker 相关的线程轨道

分析要点

图表渲染中…

关键指标解读:

指标含义优化方向
Self Time函数自身执行时间(不含子调用)定位真正的计算热点
Total Time包含子调用的总耗时了解整体调用链路
Idle 时间Worker 空闲等待时长考虑合并任务减少唤醒次数
Main Thread BlockingWorker 是否导致主线程卡顿检查 GC 或大内存分配

实战:检测 Worker 中的长任务

javascript
// 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 拥有独立的内存空间,但其内存泄漏同样会导致页面整体性能下降甚至崩溃。

常见泄漏模式

javascript
// ❌ 泄漏模式 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 面板快照对比

  1. PerformanceMemory 勾选框(或直接打开 Memory 面板)
  2. 选择 Heap snapshot 类型
  3. 在 Worker 执行前后分别拍摄快照
  4. 切换到 Comparison 视图,观察对象增量
  5. 关注 (array)(typed array)Object 等类型的 Delta
javascript
// 手动触发 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 是否存活
javascript
// 完整的调试增强 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 渲染阻塞主线程的问题。

<h4>009-offscreen-canvas-worker.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>【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 渲染的痛点在于所有绘制操作(fillRectdrawImagegetImageData 等)都在主线程执行,当画布尺寸较大或绘制内容复杂时,会导致界面卡顿。OffscreenCanvas 将渲染工作转移到 Worker 中:

plaintext
传统模式:
  主线程: UI 交互 + Canvas 绘制 → 😫 卡顿
 
OffscreenCanvas 模式:
  主线程: UI 交互(流畅)😊
  Worker: Canvas 绘制(独立线程)→ 位图回传 → 主线程合成显示

transferControlToOffscreen() 基础用法

html
<!-- index.html -->
<canvas id="mainCanvas" width="800" height="600"></canvas>
javascript
// 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 完成
javascript
// 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 中进行图像处理的管线:

javascript
// 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
}
javascript
// 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 兼容性与注意事项

浏览器OffscreenCanvastransferControlToOffscreen
Chrome 69+
Firefox 105+
Safari 16.4+
Edge 79+
重要限制
  1. transferControlToOffscreen() 只能调用一次:一旦转移控制权,主线程不能再对该 <canvas> 元素调用 getContext() 等方法
  2. 部分 Canvas API 在 OffscreenCanvas 中不可用:如 isPointInPath()measureText() 等涉及布局计算的 API
  3. toDataURL() / toBlob() 不可用:如需导出图片,应在 Worker 中使用 convertToBlob() 替代
  4. WebGL 上下文同样支持offscreen.getContext('webgl2') 可以在 Worker 中使用 WebGL 渲染

最佳实践总结

<h4>004-worker-error-handling.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>【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 的任务
  • 需要访问 localStoragesessionStorage 的任务

2. 代码组织建议

javascript
// ✅ 好的实践:使用类封装 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. 错误处理模式

javascript
// 统一的错误处理模式
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(精简版)
  • console
  • JSONMathDate 等标准对象
  • fetchXMLHttpRequest(网络请求)
  • setTimeoutsetInterval(定时器)

Worker 不能访问:

  • windowdocument(DOM 相关)
  • parenttopframes
  • localStoragesessionStorage

Q2: 如何检测浏览器是否支持 Web Workers?

javascript
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?

不能直接传递函数。如果需要传递逻辑,可以:

  1. 传递函数字符串,在 Worker 中使用 eval()(不推荐)
  2. 传递配置对象,Worker 根据配置执行不同逻辑
  3. 使用 importScripts 导入包含函数的脚本
javascript
// ❌ 不能这样做
worker.postMessage({ fn: function () {} })
 
// ✅ 可以这样做
worker.postMessage({
  action: "calculate",
  params: [1, 2, 3]
})

Q5: Worker 中的 this 指向什么?

在 Worker 中,this 指向 self(Worker 的全局对象)。建议使用 self 而不是 this,代码更清晰。

javascript
// 在 Worker 中
console.log(this === self) // true
self.onmessage = function (event) {
  // 使用 self 更明确
}

Q6: 如何实现 Worker 之间的通信?

使用 MessageChannel 可以在两个 Worker 之间建立直接通信通道,详见 MessageChannel 部分

Worker 不能直接访问 Cookie,但可以通过以下方式:

  1. 在主线程中读取 Cookie,然后通过 postMessage 传递给 Worker
  2. 在 Worker 中使用 fetch 请求时,Cookie 会自动包含在请求头中(如果设置了 credentials: 'include'

Q8: 如何调试 Worker 代码?

  1. 在 Chrome DevTools 的 Sources 面板中找到 Worker 脚本
  2. 设置断点进行调试
  3. 使用 console.log 输出调试信息
  4. 在 Console 面板中可以看到 Worker 的输出

详见 Web Worker 调试与性能分析 章节。

Q9: Worker 会影响页面性能吗?

Worker 在独立线程中运行,不会阻塞主线程,但会占用系统资源:

  • 每个 Worker 会消耗内存
  • 过多的 Worker 可能导致 CPU 过载
  • 建议 Worker 数量不超过 CPU 核心数

Q10: 如何处理 Worker 加载失败?

javascript
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 最常见的限制之一,以下是成熟的替代方案:

方案一:主线程代理模式(最常用)

javascript
// 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 预渲染

javascript
// 在主线程预先准备模板和数据
// 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 / 库辅助

javascript
// 使用轻量级虚拟 DOM 库(如 uhtml、lighterhtml 等)
// 这些库支持在 Worker 中构建 DOM 树,然后一次性同步到主线程

方案四:CSS-in-JS + 数据驱动

javascript
// 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 问题。

问题诊断

javascript
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
# Nginx 配置
location /workers/ {
  add_header Access-Control-Allow-Origin * always;
  # 或者指定具体域名
  # add_header Access-Control-Allow-Origin https://your-app.com always;
}
javascript
// 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 脚本放到同源服务器

javascript
// 开发环境: Vite/Webpack 会自动处理
import MyWorker from './worker?worker'
const worker = new MyWorker()
 
// 生产环境: 将 worker.js 打包到同域名下
const worker = new Worker('/assets/worker-[hash].js')

方案 C:使用 Blob Worker 绕过(临时方案)

javascript
// 从 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)

最佳实践

javascript
// ✅ 推荐: 根据 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 资源占用

javascript
// 使用 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 应用性能的重要工具,通过将计算密集型任务移到后台线程,可以显著改善用户体验。关键要点:

  1. 选择合适的场景:不是所有任务都需要 Worker
  2. 合理管理生命周期:及时创建和销毁 Worker
  3. 优化数据传输:使用 Transferable Objects 减少复制开销
  4. 完善的错误处理:确保 Worker 错误不会影响主应用
  5. 性能监控:跟踪 Worker 的执行时间和资源使用
  6. 善用构建工具:Vite/Webpack 提供了便捷的 Worker 集成方案
  7. 利用 OffscreenCanvas:将渲染任务完全移入 Worker 线程
  8. 重视调试能力:熟练运用 DevTools 的 Threads/Memory 面板

通过合理使用 Web Workers,可以构建更流畅、响应更快的 Web 应用。

补充示例

<h4>001-worker-calculation.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>【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>斐波那契数列 &amp; 素数计算 — 验证主线程不阻塞</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>
<h4>005-worker-terminate-timeout.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>【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>
<h4>006-shared-worker-demo.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>【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>
<h4>007-worker-inline-methods.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>【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>
<h4>008-worker-importscripts.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>【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>
<h4>010-worker-message-performance.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>【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>