WebAssembly 实战 —— 前端提速的黑科技
WebAssembly 早在 2015 年就开始萌芽,直到 2018 年才得到各大浏览器的广泛支持
从最初的狂热到现在的冷静,WebAssembly(简称 wasm )到底给前端开发带来了什么?
一、什么是 WebAssembly
首先必须明确一点,wasm 不是一个新的框架或者库,而是一种全新的语法,和 HTML、CSS、JavaScript 平级
但 wasm 的出现绝不是要让它成为一门新的编程语言(不需要手写 .wasm 文件,所以不用想着取代 JavaScript)
而是被设计为一个编译目标(就像 windows 系统中的 .exe 文件),为诸如 C、C++ 和 Rust 等语言提供一个高效的编译目标
所以 WebAssembly 只是提供了在浏览器上 (or node.js) 运行非 JavaScript 编程语言的能力
事实上,大多数编写 wasm 的开发人员并不是纯前端工程师
二、实战
WebAssembly 有两种文件格式:.wasm 和 .wat
其中 .wasm 文件以二进制的可执行文件,是编译后的结果
而 .wat 是一种文本格式(就像 .js 文件),可用于 debug,最终需要编译为 .wasm 文件
上文已经说过, WebAssembly 被设计为一个编译目标,所以不需要手写 .wat 文件
而是通过编译工具将 C / C++ / Rust 编译为 .wasm
// .wat 文件长这个样子:
(module
(func $fac (param f64) (result f64)
local.get 0
f64.const 1
f64.lt
if (result f64)
f64.const 1
else
local.get 0
local.get 0
f64.const 1
f64.sub
call $fac
f64.mul
end)
(export "fac" (func $fac)))
1. Rust → WebAssembly
接下来就感受一下将 Rust 编译为 WebAssembly 的过程
首先需要安装 Rust 环境,建议查看官方文档的介绍,如果是 macOS 或者 Linux 可以直接运行这一行代码:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装结束后可以通过 cargo --version 检查是否安装成功
然后安装构建工具 wasm-pack
cargo install wasm-pack
准备就绪,接下来在合适的目录下创建项目:
cargo new --lib hello-wasm
这里的 Cargo.toml 就相当于前端应用的 package.json,接下来将 Cargo.toml 改为这样:
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
然后将 src/lib.rs 的内容改为:
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib_recursive(n: usize) -> usize {
if n <= 2 {
return 1;
} else {
return fib_recursive(n - 2) + fib_recursive(n - 1);
}
}
现在我们已经使用 Rust 实现了一个斐波拉契数列的累加函数 fib_recursive(虽然是通过未优化的递归实现的)
接下来在项目的根目录下执行 wasm-pack build,生成 .wasm 文件
2. 在 node 环境中使用 wasm
上面已经编译出了 .wasm 文件,可以直接读取
// index.js
const fs = require('fs');
const src = new Uint8Array(fs.readFileSync('./pkg/hello_wasm_bg.wasm'));
WebAssembly.instantiate(src)
.then((result) => {
const { fib_recursive } = result.instance.exports;
console.log('fib_recursive(10) ====> ', fib_recursive(10));
})
.catch((e) => console.error(e));
接着在 node 环境中执行 index.js
3. 在 web 项目中使用 wasm
目前 <script type='module'> 或 ES6 import 语句还不支持 wasm
// 或者使用 vite-plugin-wasm 等插件
在 web 端可以通过 fetch 或 XMLHttpRequest 来加载 wasm
这里以 React 项目为例:
import React, { useEffect } from 'react';
// pkg 位于 public 目录下
const WASM_PKG_PATH = '/pkg/hello_wasm_bg.wasm';
// 加载并实例化 wasm
function fetchAndInstantiate(url, importObject) {
return fetch(url)
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => results.instance);
}
const Demo = () => {
useEffect(() => {
fetchAndInstantiate(WASM_PKG_PATH).then((result) => {
const { fib_recursive } = result.exports;
console.log('fib_recursive(10) ====> ', fib_recursive(10));
});
}, []);
return <h1>Hello WebAssembly</h1>;
};
export default Demo;
4. 性能对比
接下来在 node 环境对比一下 js 与 wasm 的性能差异
// 测试函数, 对比 n = 40 的情况下函数执行的时间
export default function testing(fn, n = 40) {
const timer = 'testing';
console.time(timer);
const result = typeof fn === 'function' && fn(n);
console.log('计算结果:', result);
console.timeEnd(timer);
}
先用 js 实现上面的斐波那契数列累加函数(依然是未优化的递归版本)
// js-fib.js
function jsFibRecursive(n) {
return n <= 2 ? 1 : jsFibRecursive(n - 2) + jsFibRecursive(n - 1);
}
testing(jsFibRecursive);
然后改为 rust 编译的 wasm 版本
// rust-fib.js
const fs = require('fs');
const src = new Uint8Array(fs.readFileSync('./pkg/hello_wasm_bg.wasm'));
WebAssembly.instantiate(src)
.then((result) => {
const { fib_recursive } = result.instance.exports;
testing(fib_recursive);
})
.catch((e) => console.error(e));
可以看到,同样的逻辑,wasm 的耗时只有 js 版本的 25% 左右
这个数据在不同的环境下会有所差异,但 wasm 更快已是不争的事实
以上的实战代码用到了 JavaScript 中内置的全局对象 WebAssembly
这是 js 与 wasm 交互的胶水层,详细介绍可以查看 MDN 文档
三、应用前景
通过以上内容可以看出,wasm 的优点在于一个字——快!
但这仅限于 wasm 的沙箱之内,而 wasm 与 js 的交互相当耗时,所以在使用的时候应当注意:
尽可能将纯计算逻辑限定在 wasm 内部,应该尽量减少 js 与 wasm 的来回调用(所以业务代码不适合也不应该编译为 wasm)
概括的说,wasm 的应用场景有以下两个方面:
1. 复杂的计算可以使用 wasm 来提高性能
比如: 视频/音乐编辑、游戏引擎、AutoCAD、Figma
2. 把一些 C++ / Rust 写的 native 库移植到浏览器里来增强浏览器的能力
这是目前最适合使用 wasm 的场景
参考资料: