标签

Web Worker

Web Worker 是 HTML5 提供的一种在后台线程中运行脚本的机制。它允许网页脚本在后台线程中运行,而不会阻塞主线程,从而提高网页的性能和响应能力。例如,当一个网页需要进行大量的计算(如复杂的数据加密、图像渲染中的复杂算法等),如果在主线程中进行,会导致页面冻结,用户无法进行其他操作,如滚动页面、点击按钮等。而使用 Web Worker,这些计算任务可以放在后台线程中执行,主线程依然可以响应用户的交互操作。

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