Javascript的搅局者—Webassembly

2,458 阅读6分钟

最近在破解网站验证码的时候,图像识别速度上遇到一点瓶颈。按照我现在的代码,从获取到验证码图片到输出正确验证码字符串需要等待3秒的时间,但是3秒之后破解完黄花菜都凉了,所以我想有没有什么方法让程序执行的快一点,最后目光聚焦在了Webassembly,所以入门学习了一下Webassembly。

Webassembly是什么?

Webassembly顾名思义web+assembly,web版的汇编语言,其实它并不是一种语言,它只是为高级语言(诸如C、C++和Java)提供一个高效的编译目标。之所以和汇编能扯上关系,是因为它是接近计算机机器码的二进制字节码,并且可以在现代浏览器直接运行。

Webassembly有什么优势?

众所周知javascript是被十天设计出来的弱类型语言,整个web的性能优化史就是一个js的填坑史,JIT引擎让js的执行速度快了10倍,为了弥补JIT引擎的缺点,又出现了asm.js、TypeScript等js子集,至此web性能已经很高,但是人类的欲望是没有止境的,浏览器厂商还是再想怎么可以让web速度更快,于是Webassembly横空出世, 相比js,Webassembly体积更小、加载更快,兼容性强,执行速度快,这也是我所需要的。

Webassembly的兼容性?

使用一个新技术的时候我们首先得看一下它在浏览器上的兼容性。进入Webassembly的官方网站,在导航条下方醒目的展示着“Webassembly 1.0 has shipped in 4 major browser engines. ”。表明现代浏览器对Webassembly的支持非常友好。

再看一下具体浏览器支持情况:

从上面这张图可以看出各大浏览器对Webassembly的支持很好,PC端和手机端浏览器对Webassembly的支持率已经达到87.42%。同时NodeJS也已经全面支持Webassembly,我可以放心大胆的使用了。 ### Webassembly的如何使用? 前面已经说过Webassembly不是一种语言,而是一种编译目标,所以首先得找一个可以生成这个编译目标的编译工具,目前可以生成Webassembly的工具很多,Emscripten(它可以将C/C++编译成Webassembly)、AssemblyScript(它可以将TypeScript编译成Webassembly)、Binaryen(它可以将asm.js编译成Webassembly)、TeaVM(它可以将Java字节码编译成Webassembly)等。因为对TypeScript比较熟悉,所以选用AssemblyScript作为生成Webassembly的工具。 1. 新建一个NPM的项目 建立一个名为wasmTest的目录,目录里包含一个src文件夹index.html和package.json,目录结构入下图所示:
  1. 安装AssemblyScript 去npm搜了一下AssemblyScript模块,发现AssemblyScript 的 npm 官方模块已经停止维护(搞不懂为什么会停止维护),那只好直接从 Github 安装AssemblyScript 的模块。

在 package.json 的依赖加入 AssemblyScript 模块的 Github 来源。

"devDependencies": {
    "assemblyscript": "github:assemblyscript/assemblyscript"
}

执行cnpm install,等待安装完成用asc来看一下是否安装成功。如果显示asc的使用命令行,则说明安装成功。

也可以将AssemblyScript的Github库clone到本地,使用npm link的方式来全局安装AssemblyScript模块。
  1. 写原始代码 在src目录下新建一个index.ts,写一个计算平方和的方法吧
export function sqart (a: number): number {
    return a * a;
}
  1. 编译生成wasm文件 可以直接在终端里用asc命令+index.ts目录的方式生成wasm文件,为了后面运行起来简单,我将命令写到npm script脚本里,打开根目录的package.json文件,将script字段变为:
"scripts": {
    "build": "npm run build:optimized",
    "build:optimized": "asc src/index.ts -t dist/module.optimized.wat -b dist/module.optimized.wasm  --optimize"
  }

--optimize 代表编译时需要优化,在项目根目录执行npm run build命令开始编译,最终在dist目录下生成module.optimized.wasm和module.optimized.wat两个文件,它们分别是WebAssembly字节码文件和WebAssembly文本文件。编译完成的目录结构如图所示:

  1. 在NodeJs中使用wasm 在根目录下新建nodejs目录,在nodejs目录新建index.js和module.js,在module.js里引入wasm模块,代码如下:
const fs = require("fs");
const path = require('path');

const env = {
	memoryBase: 0,
	tableBase: 0,
	memory: new WebAssembly.Memory({
		initial: 256
	}),
	table: new WebAssembly.Table({
		initial: 2,
		element: 'anyfunc'
	}),
	abort: () => {throw 'abort';}
}

const wasm = new WebAssembly.Module(
    fs.readFileSync(path.join(__dirname, "..", "/dist/module.optimized.wasm"))
);

const mod =  new WebAssembly.Instance(wasm, {env: env})

module.exports = mod.exports;

然后在index.js里使用封装好的wasm模块,

var myModule = require("./module.js");

console.log("3 sqart is: ", myModule.sqart(3));

在根目录下运行node ./nodejs/index.js,输出下面结果:

  1. 在浏览器中使用wasm 在根目录下新建index.js,现在项目的目录结构如下:

在index.js里引入wasm模块,代码如下:

const env = {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({
        initial: 256
    }),
    table: new WebAssembly.Table({
        initial: 2,
        element: 'anyfunc'
    }),
    abort: () => {
        throw 'abort';
    }
}
/**
 * 链接wasm和js的胶水代码
 * @param {String} path wasm 文件路径
 */
function loadWebAssembly(path) {
    return fetch(path)
        .then(response => response.arrayBuffer())
        .then(buffer => WebAssembly.compile(buffer))
        .then(module => {
            // 创建 WebAssembly 实例
            return new WebAssembly.Instance(module, {env: env})
        })
}
loadWebAssembly('./dist/module.optimized.wasm')
    .then(instance => {
        const {
           sqart
        } = instance.exports
        console.log("5 sqart is: ", sqart(5));
})

在index.html里调用封装好的wasm模块,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>webassembly</title>
  </head>
  <body>
    <div id="box">
      webassembly test
    </div>
  </body>
 
  <script src="./index.js"></script>
</html>

在浏览器中打开index.html,可以看到控制台里已经打印出结果:

Webassembly和Javascript的速度对比

已经知道Webassembly在nodeJs和浏览器中如何使用,那是不是可以满足我对性能提升的要求呢,我需要做一下速度对比测试。计算斐波拉切数列是一个不错的测速方式,在src/index.ts添加计算斐波拉切数列的方法,index.ts代码如下:

export function sqart (a: number): number {
    return a * a;
}
export function fibonacci (n: number): number {
    if ( n <= 2 ) {
        return 1;
    }
    return fibonacci(n - 2) + fibonacci(n - 1);
}

在根目录执行npm run build重新生成wasm代码。 看一下在nodeJs中的速度对比,这需要对nodejs/index.js的代码做一下改造,改造后的代码如下:

var myModule = require("./module.js");

function fibonacciJS(n) {
    if ( n <= 2 ) {
        return 1;
    }
    return fibonacciJS(n - 2) + fibonacciJS(n - 1);
} 

const startTime = Date.now();

myModule.fibonacci(50);

console.log("wasm fibonacci(45) time is: ", `${Date.now() - startTime}ms` );

const jstartTime = Date.now();

fibonacciJS(50);

console.log("js fibonacci(45) time is: ", `${Date.now() - jstartTime}ms` );

运行结果如下:

经过对斐波拉切数列第45个,第48个,第50个数的计算耗时,明显能看出来wasm的运行速度比js快了近30%
在浏览器中测试结果跟在nodeJs中的结果类似,这里不再赘述,有兴趣的同学可以自己尝试一下。 由此可以得出结论,Webassembly可以满足我对验证码识别程序的性能改造要求,改造完成的结果也会第一时间向大家分享。

写在最后

有声音说Webassembly可以取代javascript,但是个人认为短时间内javascript是不可替代的,Webassembly只是javascript的一个补充和完善,它将更多的编程语言带到了web中。 最近也准备把我们java大神写的UA识别神器(对市面全部的UA识别准确率达到90%以上)转换成wasm模块,然后前端直接调用,以后就不需要为了识别一个UA向服务端端发一次请求,没有Webassembly之前,这是不敢想的。 Webassembly已经是一个标准并被四大浏览器厂商积极支持,虽然它现在还有一些不完美的地方,比如加载需要写胶水代码。但是随着时间的推移,它会越来越完善,web的性能会越来越高,未来我们可能会进入一个告别安装应用的时代,所有的应用都变成web应用,即开即用,希望这一天早日到来吧。