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。
选择决策
别纠结,按这个思路来:
- 主线程卡不卡? 卡 → 上 Worker
- Worker 里的计算够不够快? 不够 → 把热点函数编译成 WASM
- 两者都不需要? 那就别用,引入 Worker 有通信开销,WASM 有编译和加载成本
一个常见的误区是"用了 Worker 就快了"——不是的,Worker 只是换个地方跑,JS 该慢还是慢。另一个误区是"WASM 能替代 JS"——也不是,WASM 搞不了 DOM、调不了 fetch、处理不了事件循环,离了 JS 胶水代码它寸步难行。
选对工具,别选"更高级"的。