-
Notifications
You must be signed in to change notification settings - Fork 21
Description
前言
前段时间开发图像处理工具Pictool后,遇到高频的计算瓶颈。在寻找高频计算的前端能力解决方案过程中,入门学习了一下 WebAssembly 在前端中的应用。入门的过程中踩了不少坑,例如使用AssemblyScript 开发wasm时候,发现 npm 包 assemblyscript 已经不维护了,需要自己人工添加成从Github 仓库引用assemblyscript 的npm模块。
同时网上很多教程已经有点不同步,很多按照教程步骤后实现的代码跑不起来。最后参考原有网上的教程,一步步踩坑,实现了demo,同时也写下这篇文章作为笔记!
WebAssembly
什么是 WebAssembly
- 计算机是不能直接识别运行高级语言(C/C++, Java, JavaScript等)。
- 计算机能读懂是0和1的电子元件信号,对应到运行的机器码。
- 在前端浏览器领域里,JS是解释执行,也就是运行到哪就解释成机器码让计算机读懂并执行,在高频计算性能上有一定的瓶颈。
- WebAssembly 字节码是接近计算机能识别的机器码,只要运行环境有对应的虚拟机,能快速加载运行。
WebAssembly 优势
在前端主要的优势有
- 体积小
- 加载快
- 兼容强
WebAssembly 前端能力现状
- Node.js 目前已经支持了 WebAssembly
- 大部分主流浏览器厂商也支持了 WebAssembly
什么是 AssemblyScript
AssemblyScript是TypeScript的一个子集- 可以用
TypeScript语法编写功能编译成wasm,对前端来说比较友好。
快速开始
demo源码地址
如果想更快速尝试,可以直接去该 demo 仓库获取源码使用。
https://github.com/chenshenhai/assemblyscript-demo
安装 AssemblyScript
由于 AssemblyScript 的 npm 官方模块已经停止维护,所以AssemblyScript的模块需要从Github 来源安装。
在package.json的依赖加入 AssemblyScript 模块的 Github 来源
./package.json
{
// ...
"devDependencies": {
"assemblyscript": "github:assemblyscript/assemblyscript"
// ...
}
}再执行 npm install 从 Github 下载该模块到本地 node_module中
npm install编写功能代码
编写一个 斐波那契数列 函数
在 demo 的目录 ./src/index.ts 中
export function fib(num: i32): i32 {
if (num === 1 || num === 2) {
return 1;
} else {
return fib(num - 1) + fib(num - 2)
}
}编译
在 package.json 编写编译脚本
./package.json
{
// ...
"scripts": {
"build": "npm run build:untouched && npm run build:optimized",
"build:untouched": "./node_modules/assemblyscript/bin/asc src/index.ts -t dist/module.untouched.wat -b dist/module.untouched.wasm --validate --sourceMap --measure",
"build:optimized": "./node_modules/assemblyscript/bin/asc src/index.ts -t dist/module.optimized.wat -b dist/module.optimized.wasm --validate --sourceMap --measure --optimize"
// ...
},
}在项目根目录开始执行编译
npm run build后面会在 ./dist/ 目录下产生编译后的几种 wasm 文件格式
├── dist
│ ├── module.optimized.wasm
│ ├── module.optimized.wasm.map
│ ├── module.optimized.wat
│ ├── module.untouched.wasm
│ ├── module.untouched.wasm.map
│ └── module.untouched.watNode.js 使用
在 ./example/node/module.js 文件中,封装wasm的CommonJS使用模块
const fs = require('fs');
const path = require('path');
const wasmFile = fs.readFileSync(path.join(__dirname, '..', '..', './dist/module.optimized.wasm'))
const wasm = new WebAssembly.Module(wasmFile, {});
module.exports = new WebAssembly.Instance(wasm, {
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256,
maximum: 512,
}),
table: new WebAssembly.Table({
initial: 0,
maximum: 0,
element: 'anyfunc',
}),
abort: console.log,
},
}).exports;Node.js 使用
const mod = require('./module');
const result = mod.fib(40);
console.log(result);执行 Node.js 的 wasm 引用
输出结果会是
102334155浏览器使用
在 ./example/browser/ 目录下部署浏览器访问的服务
├── dist
│ ├── module.optimized.wasm
│ └── module.untouched.wasm
├── example
│ ├── browser
│ │ ├── demo.js
│ │ ├── index.html
│ │ └── server.js临时浏览器可访问的服务,这里用 koa 来搭建服务
具体实现在 ./example/browser/server.js 文件中
const Koa = require('koa')
const path = require('path')
const static = require('koa-static')
const app = new Koa()
const staticPath = './../../'
app.use(static(
path.join( __dirname, staticPath)
))
app.listen(3000, () => {
console.log('[INFO]: server starting at port 3000');
console.log('open: http://127.0.0.1:3000/example/browser/index.html')
})浏览器使用 wasm 模块
具体实现在 ./example/browser/demo.js 文件中实现
const $body = document.querySelector('body');
fetch('/dist/module.optimized.wasm')
.then(res => res.arrayBuffer())
.then((wasm) => {
return new WebAssembly.instantiate(wasm, {
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256,
maximum: 512,
}),
table: new WebAssembly.Table({
initial: 0,
maximum: 0,
element: 'anyfunc',
}),
abort: console.log,
},
})
}).then(mod => {
const result = mod.instance.exports.fib(40);
console.log(result)
});访问页面在 ./example/browser/index.html 中
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
</body>
<script src="demo.js"></script>
</html>启动服务
node ./example/browser/server.js浏览器访问页面
http://127.0.0.1:3000/example/browser/index.html
浏览器会出现结果
102334155性能测试
Node.js 对比测试
const mod = require('./module');
const start = Date.now();
mod.fib(40)
// 打印 Node.js 环境下 wasm 计算 斐波那契数列 参数为40 的耗时结果
console.log(`nodejs-wasm time consume: ${Date.now() - start} ms`)
// 原生Node.js实现的 斐波那契数列 函数
function pureFib(num) {
if (num === 1 || num === 2) {
return 1;
} else {
return pureFib(num - 1) + pureFib(num - 2)
}
}
const startPure = Date.now()
pureFib(40);
// 打印 Nodejs环境下 原生js 计算 斐波那契数列 参数为40 的耗时结果
console.log(`nodejs-js time consume: ${Date.now() - startPure} ms`)测试结果
- Node.js环境下,原生js 执行耗时
833 ms - Node.js环境下,wasm 执行耗时
597 ms - 对比下来,wasm 计算
斐波那契数列比 js 执行快了接近30%
浏览器对比测试
const $body = document.querySelector('body');
fetch('/dist/module.optimized.wasm')
.then(res => res.arrayBuffer())
.then((wasm) => {
return new WebAssembly.instantiate(wasm, {
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({
initial: 256,
maximum: 512,
}),
table: new WebAssembly.Table({
initial: 0,
maximum: 0,
element: 'anyfunc',
}),
abort: console.log,
},
})
}).then(mod => {
const start = Date.now();
mod.instance.exports.fib(40);
const logWasm = `browser-wasm time consume: ${Date.now() - start} ms`;
$body.innerHTML = $body.innerHTML + `<p>${logWasm}</p>`
// 打印 浏览器环境下 wasm 计算 斐波那契数列 参数为40 的耗时结果
console.log(logWasm)
});
// 打印 浏览器环境下 原生js 计算 斐波那契数列 参数为40 的耗时结果
function pureFib(num) {
if (num === 1 || num === 2) {
return 1;
} else {
return pureFib(num - 1) + pureFib(num - 2)
}
}
const startPure = Date.now()
pureFib(40);
const logPure = `browser-js time consume: ${Date.now() - startPure} ms`;
$body.innerHTML = $body.innerHTML + `<p>${logPure}</p>`
console.log(logPure);测试结果
- Chrome浏览器环境下,原生js 执行耗时
884 ms - Chrome浏览器环境下,wasm 执行耗时
612 ms - 对比下来,wasm 计算
斐波那契数列比 js 执行快了也是接近30%
从上述 Node.js 和 Chrome 环境下运行 wasm 和 原生js 的对比中,wasm的在高频计算的场景下,耗时的确是比原生js低,同时都是接近 30% 的计算性能提升。


