Talk is cheap. Show me your code

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 的场景

 

 

参考资料:

《MDN: WebAssembly 概念》

《WebAssembly 中文文档》 

posted @ 2022-11-09 14:29  Wise.Wrong  阅读(3028)  评论(0编辑  收藏  举报