Skip to Content
全部文章实践出真知密集运算型选择WASM还是WebWorker?

密集运算型任务选WASM还是WebWorker?

前言

最近要做一个需求,要在前端读取xlsx文件,校验内部的数据,这个文档数据量会在1w条左右,根绝业务需求会产生多种校验规则,比如数据不能重复, 选择的文件要是符合要求的格式,某些字段有特殊的校验规则等,在数据量达到一定程度的时候就比较考验你写的校验算法性能,由于js单线程的特性, 直接写到js中可能会以为过长的计算时间(在不同性能的设备上表现会有所不同)而阻断页面渲染,用户看起来就像是卡住了。

于是我就需要考虑使用webworker或者wasm来处理这类密集运算型的任务,webworker是js的多线程实现,能够将js的计算任务放到一个独立的线程中去执行, 而wasm是编译型语言的二进制格式,能够将编译型语言的性能优势带到js中来,他们的共同特点就是都不会在主线程执行,避开影响页面渲染刷新。

我为了验证一下这两者的性能差异,做了一个简单的对比测试,测试的内容是在两边计算斐波拉契数列。

测试webworker性能

Web Worker 是一种在浏览器中实现多线程的技术,允许你在主线程之外创建后台线程,执行耗时的计算任务,而不会阻塞用户界面的渲染和交互。

测试代码:

const start = performance.now(); const worker = new Worker(URL.createObjectURL(new Blob([` self.onmessage = function (e) { const funcString = e.data.func; const args = e.data.args; const func = new Function('return ' + funcString)(); const result = func(...args); self.postMessage(result); }; `], { type: 'application/javascript' }))); const validFunction = (totalNum: number) => { function fibonacci(n: number): number { if (n <= 1) { return n; } return fibonacci(n - 1) + fibonacci(n - 2); } const result = fibonacci(totalNum); return result; } const functionString = validFunction.toString(); worker.postMessage({ func: functionString, args: [num] }); worker.onmessage = function (e) { const end = performance.now(); console.log('Worker 计算n='+num+',得到结果'+e.data+',耗时:', end - start); resolve(e.data); worker.terminate(); }; worker.onerror = function (error) { console.error('Worker 发生错误:', error.message); reject(error); };

测试结果

测试1-46的斐波拉契数列,需要的耗时如下:

n结果耗时(ms)
n=1得到结果1耗时: 6
n=2得到结果1耗时: 5
n=3得到结果2耗时: 8
n=4得到结果3耗时: 6
n=5得到结果5耗时: 6
n=6得到结果8耗时: 5
n=7得到结果13耗时: 5
n=8得到结果21耗时: 3
n=9得到结果34耗时: 9
n=10得到结果55耗时: 4
n=11得到结果89耗时: 4
n=12得到结果144耗时: 2
n=13得到结果233耗时: 5
n=14得到结果377耗时: 5
n=15得到结果610耗时: 7
n=16得到结果987耗时: 5
n=17得到结果1597耗时: 6
n=18得到结果2584耗时: 5
n=19得到结果4181耗时: 5
n=20得到结果6765耗时: 6
n=21得到结果10946耗时: 6
n=22得到结果17711耗时: 7
n=23得到结果28657耗时: 5
n=24得到结果46368耗时: 8
n=25得到结果75025耗时: 8
n=26得到结果121393耗时: 11
n=27得到结果196418耗时: 13
n=28得到结果317811耗时: 16
n=29得到结果514229耗时: 13
n=30得到结果832040耗时: 28
n=31得到结果1346269耗时: 38
n=32得到结果2178309耗时: 54
n=33得到结果3524578耗时: 72
n=34得到结果5702887耗时: 77
n=35得到结果9227465耗时: 142
n=36得到结果14930352耗时: 213
n=37得到结果24157817耗时: 326
n=38得到结果39088169耗时: 523
n=39得到结果63245986耗时: 816
n=40得到结果102334155耗时: 1309
n=41得到结果165580141耗时: 2093
n=42得到结果267914296耗时: 3388
n=43得到结果433494437耗时: 5679
n=44得到结果701408733耗时: 8742
n=45得到结果1134903170耗时: 13986
n=46得到结果1836311903耗时: 22875

测试wasm性能

WebAssembly(Wasm)是一种二进制指令格式,简单来说就是浏览器可以运行的二进制代码,就像早些年ie浏览器里面的activex控件一样, wasm是编译型语言的二进制格式, 能够将编译型语言的性能优势带到js中来,wasm的运行速度比js快很多倍,wasm的二进制格式也比js的文本格式小很多。

测试代码

本次我使用rust来编译wasm测试,测试代码如下:

use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn fibonacci_wasm(n: u32) -> u32 { if n <= 1 { n } else { fibonacci_wasm(n - 1) + fibonacci_wasm(n - 2) } }

使用 wasm-pack build --target web --release 命令编译成wasm文件,使用 wasm-bindgen 来生成js的绑定代码。

最后得到一个编译好的wasm文件和一个js的绑定文件,

1

其中的js文件就是wasm的绑定代码,wasm文件就是编译好的二进制文件。另外两个文件是wasm的类型声明文件和wasm的js绑定代码的类型声明文件。

我把编译产出目录pkg拷贝到自己的测试项目中,因为打包产出有package.json:

{ "name": "fibonacci-wasm", "type": "module", "version": "0.1.0", "files": [ "fibonacci_wasm_bg.wasm", "fibonacci_wasm.js", "fibonacci_wasm.d.ts" ], "main": "fibonacci_wasm.js", "types": "fibonacci_wasm.d.ts", "sideEffects": [ "./snippets/*" ] }

所以我在我的测试项目中的pacage.json直接使用这种方式安装:

"fibonacci-wasm": "file:src/pkg",

为了解决静态检查部识别包,执行一次 npm i

使用时直接使用了:

import init from 'fibonacci-wasm'; init().then((wasm: any) => { const validFunction = wasm.fibonacci_wasm; const start = performance.now(); const result = validFunction(num); const end = performance.now(); console.log('WASM 计算n='+num+',得到结果'+result+',耗时:', end - start); return result; });

测试结果

n结果耗时(ms)
n=1得到结果1耗时: 0
n=2得到结果1耗时: 0
n=3得到结果2耗时: 0
n=4得到结果3耗时: 0
n=5得到结果5耗时: 0
n=6得到结果8耗时: 0
n=7得到结果13耗时: 0
n=8得到结果21耗时: 0
n=9得到结果34耗时: 0
n=10得到结果55耗时: 0
n=11得到结果89耗时: 0
n=12得到结果144耗时: 0
n=13得到结果233耗时: 0
n=14得到结果377耗时: 0
n=15得到结果610耗时: 0
n=16得到结果987耗时: 0
n=17得到结果1597耗时: 0
n=18得到结果2584耗时: 0
n=19得到结果4181耗时: 0
n=20得到结果6765耗时: 1
n=21得到结果10946耗时: 0
n=22得到结果17711耗时: 1
n=23得到结果28657耗时: 0
n=24得到结果46368耗时: 1
n=25得到结果75025耗时: 1
n=26得到结果121393耗时: 1
n=27得到结果196418耗时: 1
n=28得到结果317811耗时: 5
n=29得到结果514229耗时: 6
n=30得到结果832040耗时: 10
n=31得到结果1346269耗时: 10
n=32得到结果2178309耗时: 25
n=33得到结果3524578耗时: 25
n=34得到结果5702887耗时: 51
n=35得到结果9227465耗时: 70
n=36得到结果14930352耗时: 101
n=37得到结果24157817耗时: 150
n=38得到结果39088169耗时: 212
n=39得到结果63245986耗时: 359
n=40得到结果102334155耗时: 567
n=41得到结果165580141耗时: 904
n=42得到结果267914296耗时: 1456
n=43得到结果433494437耗时: 2314
n=44得到结果701408733耗时: 3749
n=45得到结果1134903170耗时: 6031
n=46得到结果1836311903耗时: 9893

结果评比

因为我电脑是m1pro芯片,性能还算强劲,但是计算到n=46时,已经比较耗时了,斐波拉契计算n每增加1,计算时长增加60%左右,继续计算下去也没有意义了, 所以只测试到46,实际上很多电脑性能是不如m1pro芯片的,所以在不同性能的电脑上,wasm和webworker的耗时不会和我的一样。

我们来看看结果横向对比:

n结果webworker耗时(ms)wasm耗时(ms)
n=1得到结果1耗时: 6耗时: 0
n=2得到结果1耗时: 5耗时: 0
n=3得到结果2耗时: 8耗时: 0
n=4得到结果3耗时: 6耗时: 0
n=5得到结果5耗时: 6耗时: 0
n=6得到结果8耗时: 5耗时: 0
n=7得到结果13耗时: 5耗时: 0
n=8得到结果21耗时: 3耗时: 0
n=9得到结果34耗时: 9耗时: 0
n=10得到结果55耗时: 4耗时: 0
n=11得到结果89耗时: 4耗时: 0
n=12得到结果144耗时: 2耗时: 0
n=13得到结果233耗时: 5耗时: 0
n=14得到结果377耗时: 5耗时: 0
n=15得到结果610耗时: 7耗时: 0
n=16得到结果987耗时: 5耗时: 0
n=17得到结果1597耗时: 6耗时: 0
n=18得到结果2584耗时: 5耗时: 0
n=19得到结果4181耗时: 5耗时: 0
n=20得到结果6765耗时: 6耗时: 1
n=21得到结果10946耗时: 6耗时: 0
n=22得到结果17711耗时: 7耗时: 1
n=23得到结果28657耗时: 5耗时: 0
n=24得到结果46368耗时: 8耗时: 1
n=25得到结果75025耗时: 8耗时: 1
n=26得到结果121393耗时: 11耗时: 1
n=27得到结果196418耗时: 13耗时: 1
n=28得到结果317811耗时: 16耗时: 5
n=29得到结果514229耗时: 13耗时: 6
n=30得到结果832040耗时: 28耗时: 10
n=31得到结果1346269耗时: 38耗时: 10
n=32得到结果2178309耗时: 54耗时: 25
n=33得到结果3524578耗时: 72耗时: 25
n=34得到结果5702887耗时: 77耗时: 51
n=35得到结果9227465耗时: 142耗时: 70
n=36得到结果14930352耗时: 213耗时: 101
n=37得到结果24157817耗时: 326耗时: 150
n=38得到结果39088169耗时: 523耗时: 212
n=39得到结果63245986耗时: 816耗时: 359
n=40得到结果102334155耗时: 1309耗时: 567
n=41得到结果165580141耗时: 2093耗时: 904
n=42得到结果267914296耗时: 3388耗时: 1456
n=43得到结果433494437耗时: 5679耗时: 2314
n=44得到结果701408733耗时: 8742耗时: 3749
n=45得到结果1134903170耗时: 13986耗时: 6031
n=46得到结果1836311903耗时: 22875耗时: 9893

可以看到,wasm的性能比webworker快了很多,js在哪怕n=1,他也要耗时几毫秒,在运算量不大的情况下,虽然有差距,但是可以忽略,当n达到35时(不同cpu性能有差异), 耗时差距变大,会有明显感知差异。越往后差距越大,wasm的性能优势越明显。

从上面测试结果可以看到wasm在计算性能上比webworker要快得多得多,虽然上面结果只快1倍,原因主要是rust最强的是高效的内存、原生编译代码,没有动态类型开销, 这里的测试函数使用的是递归算法而不是迭代算法,并不是展现wasm性能的最佳场景。不论怎么说,wasm的性能都比js要快得多。

实际应用场景怎么选?

根据上面测试结果,实际应用中首选wasm来处理密集运算型任务呢?答案是:不一定。

在实际的开发中,使用wasm对团队来说成本升高,主要体现在:

  1. 新语言的学习成本
  2. 跨语言调用的复杂性、调试不便利

相比之下,webworker的优势在于:

  1. 简单易用,不需要学习新语言
  2. 可以直接使用js的api、js的依赖库

究竟选择webworker还是wasm,还是要看具体的业务场景和团队的技术栈,比如说虽然worker性能差一些,但是我会跟你说处理1w条xlsx数据的校验,最后我实测 也仅仅花了几百毫秒而已,完全可以接受的范围内,加入有一天我的需求是校验100w条数据,那我肯定就考虑wasm而不是webworker了。

另外还有一些使用wasm的情况是因为移植需要,比如c++的库需要直接应用到web上。还比如我曾经做过一个地图应用,根据gps在地图上画点画线,当gps数据太多的时候, 加上mapbox性能就不太好了,这种场景也适合使用wasm来处理一些运算逻辑。

最后编辑于

hi