认识 WebAssembly 与 Rust 实践

简介: 作者基于 WebAssembly 的兴趣写下本文,提供了一种未来在业务中遇到性能问题时的优化手段和思路。


简介

先来说下在 WebAssembly(后续称WASM) 官网上的介绍,主要有四点:

  1. 高效:WASM 有一套完整的语义,实际上 WASM 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件的能力以达到原生语言的执行效率
  2. 安全:WASM 运行在一个内存安全,沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在 Web 环境中 ,WASM 将会严格遵守同源策略以及浏览器安全策略
  3. 开放:WASM 设计了一个非常规整的文本格式用来、调试、测试、实验、优化、学习、教学或者编写程序。可以以这种文本格式在 Web 页面上查看 WASM 模块的源码
  4. 标准:WASM 在 Web 中被设计成无版本、特性可测试、向后兼容的。WASM 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。WASM 不仅可以运行在浏览器上,也可以运行在非 Web 环境下(如 Node.js、Deno、物联网设备等。


 发展历史


  1. 2013年:Asm.js 发布,虚幻引擎被编译成 Asm.js 移植到浏览器中,这是 WASM 的前身
  2. 2015年:各大浏览器厂商开始合作开发 WASM,成立 W3C WebAssembly Community Group
  3. 2017年:各大浏览器开始支持 WASM
  4. 2018年:2月发布首个公开的草案
  5. 2019年:WASM 发布 1.0 正式版
  6. 2022年:4月发布了 2.0 的草案


兼容性

image.png

可以看到目前的主流浏览器:Chrome、Edge、Safari、Firefox、Opera 都已经支持,Safari 11版本(对应 IOS 11)以上的移动端对于 WASM 的支持也比较好了,如果是低于 IOS 11 以下的系统就需要做逻辑兜底的处理了。所以如果是 B 端的项目,可以放心大胆的去在项目中进行落地,如果是 C 端的项目,可能会有一小部分用户的系统会不支持,这时候可以使用 wasm2js 工具来做代码转换兜底。


在正式去了解 WASM 之前我们先来了解一下 LLVM 👇



LLVM


LLVM 是模块化和可重用的编译器和工具链技术的集合,它是由 C++ 编写的。尽管叫做 LLVM,但它跟传统虚拟机几乎没啥关系。“LLVM” 这个名称本身并不是首字母缩写(并不是 Low Level Virtual Machine),LLVM 就是它的全称。它用于优化以任意的编程语言编写的程序的编译时间、链接时间、运行时间以及空闲时间,经过各种优化后,输出一套适合编译器系统的中间语言,目前采用它来做转换的语言有很多:Swift、Object-C、C#、Rust、Java字节码等。WASM 编译器底层也使用了LLVM 去将原生代码(如Rust、C、C++等)转换成 WASM 二进制代码。

 编译器


编译器包括三部分:

  1. 前端:负责处理源语言
  2. 优化器:负责优化代码
  3. 后端:负责处理目标语言


 前端


前端在接收到代码的时候就会去解析它,然后检查代码是否有语法或语法问题,然后代码就会转换成中间表示产物(intermediate representation) IR。

 优化器


优化器会去分析 IR 并将其转换成更加高效的代码,很少有编译器会有多个中间产物。优化器相当于一个中间产物到中间产物的转换器,其实就是在中间做了一层加工优化处理,优化器包括移除冗余的计算,去掉执行不到的冗余代码,还有一些其它的可以进行优化的选项。

 后端


后端会接收中间产物并转换它到其它语言(如机器码),它也可以链接多个后端去转换代码到一些其它语言。为了产生高效的机器代码,后端应该理解执行代码的体系结构。

 LLVM 的功能


LLVM 的核心是负责提供独立于源、目标的优化,并为许多 CPU 架构生成代码。这使得语言开发人员可以只创建一个前端,从源语言生成 LLVM 兼容的 IR 或 LLVM IR。LLVM 不是一个单一项目,它是子项目和其他项目的集合。

  1. LLVM 使用一种简单的低级语言,风格类似C语言
  2. LLVM 是强类型的
  3. LLVM 有严格定义的语义
  4. LLVM 具有精确的垃圾回收
  5. LLVM 提供了各种优化,可以根据需求选择。它具有积极的、标量的、过程间的、简单循环的和概要文件驱动的优化
  6. LLVM 提供了各种编译模型。分别是链接时间、安装时间、运行时和脱机
  7. LLVM 为各种目标架构生成机器码
  8. LLVM 提供 DWARF 调试信息(DWARF 是一种调试文件格式,许多编译器和调试器都使用它来支持源代码级别的调试)


了解了 LLVM 我们就正式进入 WASM 的内容介绍。


WASM 和 JS


它被设计用于高效执行和紧凑表达,它可以以接近原生代码的速度在所有 JS 引擎上执行 (手机、电脑浏览器、Node.js)。每个 WASM 文件都是一个高效、最优且自给自足的模块,称为 WASM 模块,它运行在沙盒上,内存安全,没有权限获取超出沙盒限制以外的东西,WASM 是一个虚拟指令集结构。

▐  JavaScript 是如何执行的?

image.png

  1. 把整个文件加载完成
  2. 将代码解析成抽象语法树
  3. 解释器进行解释然后编译再执行
  4. 最后再进行垃圾回收


JavaScript 既是解释语言又是编译语言,所以 JavaScript 引擎在解析后启动执行。解释器执行代码的速度很快,但它每次解释时都会编译代码。JavaScript 引擎有监视器 (在某些浏览器中称为分析器)。监视器跟踪代码执行情况,如果一个特定的代码块被频繁地执行,那么监视器将其标记为热代码。引擎使用即时 (JIT) 编译器编译代码块。引擎会花费一些时间进行编译,比如以纳秒为单位。花在这里的时间是值得的,因为下次调用函数时,执行速度会比之前快得多,因为编译型代码比解释型代码要快,这个阶段是优化代码阶段。JavaScript 引擎增加了一(或两)层优化,监视器会持续监视代码的执行,监视器标记那些被执行频次更高的代码为高热点代码,引擎将进一步优化这段代码,这个优化需要很长时间。这个阶段产生运行速度非常快的高度优化过的代码,该阶段的优化代码执行速度要比上一段说的优化过的代码还要快得多。显然,引擎在这一阶段花费了更多时间,比如以毫秒为单位,这里耗费的时间将由代码性能和执行效率来进行补偿。JavaScript 是一种动态类型的语言,引擎所能做的所有优化都是基于类型的推断。如果推断失败,那么将重新解释并执行代码,并删除优化过的代码,而不是抛出运行时异常。JavaScript 引擎实现必要的类型检查,并在推断的类型发生变化时提取优化的代码,但是如果重新推断类型,那花在上述代码优化阶段的功夫就白费了。


开发中我们可以通过使用 TypeScript 来防止一些与类型相关的问题,使用 TypeScript,可以避免一些多态代码 (接受不同类型的代码) 的出现。在 JavaScript 引擎中,只接受一种类型的代码总是比多态代码运行得快,但是如果是 TS 里带有泛型的代码,那也会被影响到执行速度。最后一步是垃圾回收,将删除内存中的所有活动对象,JavaScript 引擎中的垃圾回收采用标记清除算法,在垃圾回收过程中,JavaScript 引擎从根对象 (类似于 Node.js 中的全局对象) 开始。它查找从根对象开始引用的所有对象,并将它们标记为可访问对象,它将剩余的对象标记为不可访问的对象,最后清除不可访问的对象。


▐  WASM 是怎么执行的?

image.png

WASM 是二进制格式并且已经被编译和优化过了,首先 JS 引擎会去加载 WASM 代码,然后解码并转换成模块的内部表达(即 AST)。这个阶段是解码阶段,解码阶段要远远比 JS 的编译阶段要快。接下来,解码后的 WASM 进入编译阶段,在这个阶段,对模块进行验证,在验证期间,对代码进行某些条件检查,以确保模块是安全的,没有任何有害的代码,在验证过程中对函数、指令序列和堆栈的使用进行类型检查,然后将验证过的代码编译为机器码。由于 WASM 二进制代码已经提前编译和优化过了,所以在其编译阶段会更快,在这个阶段,WASM 代码会被转换为机器码。最后编译过的代码进入执行阶段,执行阶段,模块会被实例化并执行。在实例化的时候,JS 引擎会实例化状态和执行栈,最后再执行模块。WASM 的另一个优点是模块可以从第一个字节开始编译和实例化,因此,JS 引擎不需要等到整个模块被下载,这可以进一步提高 WASM 的性能。WASM 快的原因是因为它的执行步骤要比 JS 的执行步骤少,其二进制代码已经经过了优化和编译,并且可以进行流式编译。但是总的来说,WASM 并不是总是比原生JS 代码执行速度要快的,因为 WASM 代码和 JS 引擎交互和实例化也是要耗费时间的,所以需要考虑好使用场景,在一些简单的计算场景里,WASM 和 JS 引擎的交互时间都会远远超出其本身的执行时间,这种时候还不如直接使用 JS 来编写代码来得快,另一方面,也要减少 WASM 和 JS 引擎之间的数据交互,因为每次两者的数据交互都会耗费一定的时间。


基础部分了解了,下面我们来看一下 WASM 的开发部分。


WASM 开发语言的选择


要写 WASM 应用的话首先不能选用有 GC 的语言,不然垃圾收集器的代码也会占用很大一部分的体积,对 WASM 文件的初始化加载并不友好,比较好的选择就是 C/C++/Rust 这几个没有 GC 的语言,当然使用 Go、C#、TypeScript 这些也是可以的,但是性能也会没有C/C++/Rust 这么好。从上面几个语言来看 Rust 对于前端选手来说会稍微亲切一些,从语法上看和 TS 有一点点的相似(但是学下去还是要比 TS 难得多的), Rust 的官方和社区对于 WASM 都有着一流的支持,而且它也是一门系统级编程语言,有一个和 NPM 一样好用的包管理器 Cargo,同时 Rust 也拥有着很好的性能,用来写 WASM 再好不过了。同时它的社区热度也在不断的上升中。

image.png

Rust 提供了对 WASM 一流的支持,Rust 无需 GC 、零运行时开销的特点也让它成为了 WASM 的完美候选者。Rust 是怎么编译成 WASM 代码的:



image.png

▐  从零开始 WASM 项目


  • Rust 安装


首先需要安装好 Rust的开发环境。安装好之后控制台运行 rustc --version 显示版本号即可。

  • wasm-pack(WASM 打包器)


一个专门用于打包、发布 WASM 的工具,可以用于构建可在 NPM 发布的 WASM 工具包。当我们开发完 WASM 模块时,可以直接使用 wasm-pack publish 命令把我们开发的 WASM 包发布到 NPM 上。使用cargo install wasm-pack 命令来进行安装。

▐  开发环境搭


接下来我们举个例子从零开始搭建一个 WASM 开发目录


  • 创建 Rust 工程


首先创建 Rust 工程目录:cargo new example --lib然后在其目录下控制台运行npm init -ypackage.json 内容如下,配置好 package.json 之后先安装依赖,执行 npm install

{  "name": "example",  "version": "0.1.0",  "description": "",  "main": "index.js",  "scripts": {    "build": "rimraf dist pkg && webpack",    "start": "rimraf dist pkg && webpack-dev-server",    "test": "cargo test && wasm-pack test --headless"  },  "devDependencies": {    "@wasm-tool/wasm-pack-plugin": "^1.6.0",    "html-webpack-plugin": "^5.5.0",    "rimraf": "^3.0.2",    "webpack": "^5.75.0",    "webpack-cli": "^4.10.0",    "webpack-dev-server": "^4.11.1"  },  "keywords": [],  "author": "",  "license": "ISC"}


cargo.toml 依赖如下:


[package]categories = ["wasm"]description = ""edition = "2021"name = "example"version = "0.1.0"
[lib]# 一个动态的系统库将会产生,类似于C共享库。当编译一个从其它语言加载调用的动态库时这属性将会被使用crate-type = ["cdylib"]
[features]
[dependencies]# 用于将实体从 Rust 绑定到 JavaScript,或反过来。# 提供了 JS 和 WASM 之间的通道,用来传递对象、字符串、数组这些数据类型wasm-bindgen = "0.2.83"wee_alloc = {version = "0.4.5", optional = true}
# web-sys 可以和 JS 的 API 进行交互,比如 DOM[dependencies.web-sys]features = ["console"]version = "0.3.60"
[dev-dependencies]# 用于所有JS环境 (如Node.js和浏览器)中的 JS 全局对象和函数的绑定js-sys = "0.3.60"
# 0 – 不优化# 1 – 基础优化# 2 – 更多优化# 3 – 全量优化,关注性能时建议开启此项# s – 优化二进制大小# z – 优化二进制大小同时关闭循环向量,关注体积时建议开启此项[profile.dev]debug = true# link time optimize LLVM 的链接时间优化,false 时只会优化当前包,true/fat会跨依赖寻找关系图里的所有包进行优化# 其它选项还有 off-关闭优化,thin是fat的更快版本lto = trueopt-level = 'z'
[profile.release]debug = falselto = trueopt-level = 'z'


  • 内存分配器(可选)

上面我们在依赖中加入了 wee_alloc 这个内存分配器,对比默认的 10kb 大小的分配器,它只有 1kb 的大小,但是它要比默认的分配器速度要慢,所以默认不开启,为减少模块打包时的大小,可以使用这个内存分配器。在src/lib.rs 中使用的代码如下:

#[cfg(feature = "wee_alloc")]#[global_allocator]static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;


  • 入口文件


项目根目录下创建 index.html 文件,写入以下内容:


<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>WASM Hello World</title>  </head>  <body></body>  <script src="index.js"></script></html>


  • Webpack 配置


项目根目录下新建 webpack.config.js 并新建 js/index.js 文件用于调用 WASM 侧暴露的函数。WasmPackPlugin 这个插件会帮我们在运行 Webpack 时自动去打包 WASM ,生成可直接用于发布的 NPM 模块。


const path = require("path");const HtmlWebpackPlugin = require("html-webpack-plugin");const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const dist = path.resolve(__dirname, "dist");
module.exports = {  mode: "development",  entry: {    index: "./js/index.js",  },  output: {    path: dist,    filename: "[name].js",  },  plugins: [    new HtmlWebpackPlugin({      template: path.resolve(__dirname, "./index.html"),      inject: false,    }),    new WasmPackPlugin({      crateDirectory: __dirname,    }),  ],  experiments: {    asyncWebAssembly: true,  },  resolve: {    extensions: [".ts", ".js"],  },};


上面的所有配置都完成之后,命令行执行 npm start 即可启动项目,然后会自动生成 pkg 目录,这个就是最终可以发布打包到 NPM 上的的 WASM 库。
最终的项目目录长这个样子:


▐  Hello World


在上面的环境搭建好之后,我们就开始试着在浏览器控制台上打印出 Hello World 吧,进入到 src/lib.rs 文件,写入以下代码:

use wasm_bindgen::prelude::*;use web_sys::console;
#[wasm_bindgen]pub fn hello_world() {    console::log_1(&JsValue::from_str("Hello World!"));}


然后到 JS 侧去进行调用,在 js/index.js 文件中写入以下代码:

async function main() {  const module = await import('../pkg/index');  module.hello_world();}
main();


运行 npm start 之后,打开 localhost:8080 端口,就能看到 Hello World 被打印出来咯。


image.png

▐  JS 和 Rust 之间的数据传递


  • JS 调用 Rust

这里我们在 src/lib.rs 里写一个斐波那契函数,可以返回一个 i32 数字类型,JS 端就可以拿到对应的返回结果:

// 斐波那契数列,时间复杂度 O(2^n)#[wasm_bindgen]pub fn fib(n: i32) -> i32 {    match n {        1 => 1,        2 => 1,        _ => fib(n - 1) + fib(n - 2),        }}


然后在根目录下的 js/index.js 中编写如下代码进行调用:

async function main() {  const module = await import("../pkg/index");  console.log(module.fib(30));}
main();


控制台上就能看到对应的结果了:

image.png再看 wasm-pack 给我们生成的 WASM 胶水代码,它在 pkg/index_bg.js 中,可以看到生成的代码中已经帮我们做好了一些边界判断和异常处理,然后 JS 侧直接引入这个文件去调用我们编写好的函数即可。如果你不想使用 webpack 的插件来生成 WASM 包,也可以自己手动执行 wasm-pack build 命令来生成。

import * as wasm from './index_bg.wasm';
const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
let cachedUint8Memory0 = new Uint8Array();
function getUint8Memory0() {  if (cachedUint8Memory0.byteLength === 0) {    cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);  }  return cachedUint8Memory0;}
function getStringFromWasm0(ptr, len) {  return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));}
function _assertNum(n) {  if (typeof(n) !== 'number') throw new Error('expected a number argument');}/*** @param {number} n* @returns {number}*/export function fib(n) {  _assertNum(n);  const ret = wasm.fib(n);  return ret;}
export function __wbindgen_throw(arg0, arg1) {  throw new Error(getStringFromWasm0(arg0, arg1));};


返回数组
#[wasm_bindgen]pub fn send_array_to_js() -> Box<[JsValue]> {    vec![        JsValue::NULL,        JsValue::UNDEFINED,        JsValue::from_str("123"),        JsValue::TRUE,        JsValue::FALSE,    ]    .into_boxed_slice()}


返回对象


在 cargo.toml 的 dependencies 加入下面两个依赖:


[dependencies]serde = { version = "1.0", features = ["derive"] }serde-wasm-bindgen = "0.4"


src/lib.rs 中编写代码:


use std::collections::HashMap;use serde::{Deserialize, Serialize};use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]pub struct Obj {    pub field1: HashMap<u32, String>,    pub field2: Vec<Vec<i32>>,    pub field3: [f32; 4],    pub field4: bool,    pub field5: String,}
#[wasm_bindgen]pub fn send_obj_to_js() -> JsValue {    let mut map = HashMap::new();    map.insert(0, String::from("ex"));
    let obj = Obj {        field1: map,        field2: vec![vec![1, 2], vec![3, 4]],        field3: [1., 2., 3., 4.],        field4: true,        field5: "哈哈哈".to_string(),    };
    serde_wasm_bindgen::to_value(&obj).unwrap()}


JS 侧进行调用:

async function main() {  const module = await import('../pkg/index');  console.log(module.send_obj_to_js());  console.log(module.send_array_to_js());}
main();


打印结果:

image.png



























export class Point {  constructor(x, y) {    this.x = x;    this.y = y;  }
  get_x() {    return this.x;  }
  get_y() {    return this.y;  }
  set_x(x) {    this.x = x;  }
  set_y(y) {    this.y = y;  }
  add(p1) {    this.x += p1.x;    this.y += p1.y;  }}


我们上面创建了一个 JS 侧的 Point 对象,然后在 Rust 端我们看看如何进行调用:先去到 src/lib.rs 目录下,加入下面的代码:


// 调用 JS 中的方法#[wasm_bindgen(module = "/js2rust/point.js")]extern "C" {    pub type Point;
    #[wasm_bindgen(constructor)]    fn new(x: i32, y: i32) -> Point;
    #[wasm_bindgen(method, getter)]    fn get_x(this: &Point) -> i32;
    #[wasm_bindgen(method, getter)]    fn get_y(this: &Point) -> i32;
    #[wasm_bindgen(method, setter)] //5    fn set_x(this: &Point, x: i32) -> i32;
    #[wasm_bindgen(method, setter)]    fn set_y(this: &Point, y: i32) -> i32;
    #[wasm_bindgen(method)]    fn add(this: &Point, p: Point);}
// 这个函数 JS 侧可以继续进行调用,最终会返回一个 point 对象实例#[wasm_bindgen]pub fn test_point() -> Point {    let p = Point::new(10, 10);    let p1 = Point::new(6, 3);    p.add(p1);    p}


▐  发布


当我们调试好代码之后,就可以在 NPM 上发布我们的 WASM 包了。直接 cd 到 pkg 目录下,修改我们的 package.json 的 name 为 example-fib, 然后执行 npm publish 就可以发布到 NPM 上了,后续可以在我们自己的项目中 npm install example-fib 下载来调用:


import { fib } from "example-fib";
async function main() {  // const module = await import("../pkg/index");  // console.log(module.fib(30));
  console.log(fib(40));}
main();


我们通过 NPM 包的形式引入我们的 WASM 斐波那契函数,可以看到一样可以调用成功。
image.png(测试电脑的CPU为10代i7,不同电脑、不同浏览器的执行时间可能都会不一样)

JS 版本的 Fibonacci 函数:





function jsFib(n) {  if (n === 1 || n === 2) return 1;  return jsFib(n - 1) + jsFib(n - 2);}


从结果中我们可以看到,在时间复杂度为 O(2^n) 的算法中, WASM 的性能是要好于 JS 的,i 的值越大,WASM 的优势就会越明显,但是如果 i 的值比较小,WASM 的性能不一定比得过 JS,因为其中 JS 和 WASM 的交互就有一定的时间成本,当然这里的比较也是在 WASM 和 JS 侧数据交互比较少的情况,如果数据交互量大了,那么速度也是会受到一定的影响的,所以在业务开发中如果使用到 WASM 模块,那么就需要尽可能减少 JS 和 WASM 之间的数据传输。


同时这里也放一篇相关的文章供大家参考,这篇文章主要讲 Rust 版本的 Markdown 解析器编译到 WASM 后和 JS 版本的 Markdown 解析器做性能对比:https://sendilkumarn.com/blog/increase-rust-wasm-performance/


主要对比的是 Rust 的 comrak 库 和 JS 的 marked 库

下面贴一下作者的最终对比结果

未经过优化的 WASM 代码,WASM 表现不佳


比对结果中可以看到, WASM 在开启了优化的情况下性能比 JS 要好


▐  代码体积优化


  • WASM 内存模型


在 JS 引擎内部,WASM 和 JS 在不同的位置运行。跨越它们之间的边界进行交互是有成本的。浏览器内部用了一些手段来降低这个成本,但是当程序跨越这个边界时,这个行为很快就会成为程序的主要性能瓶颈。以减少边界跨越的方式设计 WASM 程序是很重要。但是一旦程序变大,就很难控制。为了防止边界跨越,WASM 模块附带了内存模型。WASM 模块中的内存是线性内存的向量。线性内存模型是一种内存寻址技术,其中内存被组织在一个块线性地址空间中,它也被称为扁平内存模型。线性内存模型使理解、编程和表示内存变得更容易。但是它也有巨大的缺点,例如重新排列内存中的元素需要大量的执行时间,并且会浪费大量的内存区域。在这里,内存表示一个包含未解释数据的原始字节向量。WASM 使用可调整大小的数组缓冲区来保存内存的原始字节。创建的内存可以从 JS 和 WASM 模块中进行访问和改变。


  • WASM 内存分析


使用 twiggy 这个 crate

cargo install twiggy

使用这个包可以看到相关代码大小占用以及寻找某些编译器不知道如何进行优化的冗余代码。


image.png

这样的一段代码编译成 WASM 之后,我们看一下其大小,输入命令 twiggy top -n 10 ./pkg/index_bg.wasm 对输出的 pkg/index_bg.wasm 文件进行代码分析可以看到下面的结果,top -n 10 表示取排名前十的文件,我们可以看到这个 WASM 文件总共占了 8kb 的大小,我们可以根据相关的信息来进行代码优化,越复杂的应用最后展示的信息会越明朗,因为我们这里的代码比较简单,展示出来的基本都是一些内置函数的代码大小,更多相关信息可以查看 twiggy 文档。

image.png

  • 进一步压缩体积


使用 wasm-opt这个 C++ 编写的工具可以进一步去压缩 WASM 模块的体积大小。下载完后将其解压放到 ~/.cargo/bin 目录下,然后 wasm-opt -h 之后控制台能打印出帮助信息表示安装成功了。我们拿上面说到的 8kb 的 WASM 文件试着压缩一下,在项目根目录下执行 wasm-opt -Oz pkg/index_bg.wasm -o pkg/index_opt_bg.wasm,Oz 选项代表极致压缩大小。然后查看生成的 index_opt_bg.wasm 文件,压缩前 7.86 kb,压缩后 6.12 kb。根据官网的介绍,其正常的压缩效率会在 10%~20% 左右。


image.png▐  其它WASM 开发工具


编译器可以将高级代码转换为 WASM 二进制代码,但是生成的二进制文件都是经过了相关的压缩和性能优化的。它很难理解、调试和验证 (它是一堆十六进制数)。转换 WASM 二进制到原始源代码很难。WebAssembly 二进制工具包 (WABT) 帮助将 WASM 二进制转换为人类可读的格式,例如 WASM 文本 (WAST) 格式或 C 语言原生代码。WABT 工具包在 WASM 的开发生态中很重要,是我们开发 WASM 中的重要一环。WABT(WebAssembly Binary ToolKit) 有以下的能力:

  1. wat2wasm:转换 WAST 到 WASM
  2. wasm2wat:转换 WASM 到 WAST
  3. wasm2c:转换 WASM 到 C 语言
  4. wast2json:转换 WAST 到 JSON
  5. wasm-validate:验证 WASM 是否按照规范来构建
  6. wasm-decomplie:反编译 WASM 代码到类似于 C 语言的语法的可读代码
  7. 还有一些其它的能力可以参考上面的地址


▐  WASM 开发框架


开发软件时使用 WASM 的方式有几种:

  1. 纯 WASM 实现,包括 UI 和逻辑
  2. UI 使用 HTML/CSS/JS,逻辑计算使用 WASM
  3. 复用其它语言中的库,使用 WASM 来移植到已有的 Web 软件中


如果需要使用纯 WASM 来开发应用,不同语言和 WASM 开发相关的框架:

  1. Rust:Yew(语法类似于 React)、SeedPerseus
  2. Go:Vecty、Vugu
  3. C#:Blazor


虽然现在可以用 WASM 来编写 Web 应用了,但是还存在一定的局限性,就是无法直接从 WASM 中直接操作 DOM 和一些其它的浏览器 API,还是需要通过 FFI (外部函数接口) 来进行调用 JS 提供的能力。


▐  WASM 相关的库


图片处理:Photon,这是一个高性能的 Rust 编写的图片处理库,支持 Rust 原生调用、浏览器中使用 WASM 调用、在 Node 中使用 WASM 调用。


音视频处理:ffmpeg.wasm,C语言编写的音视频处理工具,通过 WASM 移植到了浏览器内。


WASM 运行时:Wasmer,Wasmtime,其可以嵌入到任何编程语言并且可以在多种设备上去运行 WASM 。根据 Wasmer 官网介绍,quick.js 引擎的 WASM 版本也可以在一些脱离于浏览器之外的其它环境上去运行,甚至你还可以在浏览器的 V8 Js 引擎中去跑 WASM 版本的 quick.js 引擎,拿其来做一些动态代码下发的事情,同时达成套娃成就。


▐  现有的使用 WASM 编写的应用


  • PSPDFKit


其产品官网介绍了他们的 Web 版本是如何使用 WASM 进行优化的,其介绍的相关文章:优化 WASM 的启动性能,他们主要做的加载优化主要是以下的 4 个方面:

  1. 文件缓存,因为 .wasm 文件和 .js 文件类似,静态资源是从网络进行加载的,所以可以进行浏览器缓存,可以强制或者协商缓存到本地,这个一般需要服务端来配合。
  2. 使用流实例化
  3. 把已经编译好的 WASM 模块缓存到 IndexDB 中加快后续加载速度
  4. 使用对象池缓存预热实例


这是他们给出的一段主要代码:


const MODULE_VERSION = 1;
// 从 IndexDB 加载缓存const cache = await getCache('WASMCache');let compiledModule = await cache.get(MODULE_VERSION);
// 创建一个 WebAssembly.Module 实例,如果缓存中存在则直接返回缓存if (compiledModule) {    return WebAssembly.instantiate(compiledModule, imports);}
const fetchPromise = fetch('module.wasm');
let instantiatePromise;
// 检测浏览器是否支持 WebAssembly.instantiateStreaming 流式实例化const isInstantiateStreamingSupported =    typeof WebAssembly.instantiateStreaming == 'function';
if (isInstantiateStreamingSupported) {    instantiatePromise = WebAssembly.instantiateStreaming(        fetchPromise,        imports,    );} else {    // 不支持则采用原始的实例化方式    instantiatePromise = fetchPromise        .then((response) => response.arrayBuffer())        .then((buffer) => WebAssembly.instantiate(buffer, imports));}
const result = await instantiatePromise;
// 将加载结果缓存到 IndexDB 中cache.put(MODULE_VERSION, result.module);return result.instance;


其中流实例化这个方式还是比较新的特性,目前兼容性并不是特别好,所以需要做好兜底处理,从下图可以看到在 Safari 15 以上才支持。

image.png





相关文章
|
2月前
|
Rust 监控 JavaScript
抖音技术分享:飞鸽IM桌面端基于Rust语言进行重构的技术选型和实践总结
本文将介绍飞鸽IM前端团队如何结合Rust对飞鸽客户端接待能力进行的技术提升,一步步从概念验证、路径分解到分工开发,再到最后上线收益论证,并分享了其中遇到的技术挑战与经验总结等。
56 1
|
2月前
|
Rust 监控 算法
Rust中的系统性能监控与调优:提升应用效能的关键实践
随着Rust在系统级编程中的广泛应用,性能监控与调优变得尤为关键。本文介绍了在Rust中实施系统性能监控的方法,探讨了Rust应用的性能瓶颈,并提供了调优策略与最佳实践,旨在帮助开发者更有效地提升Rust应用的性能。
|
2月前
|
Rust 前端开发 JavaScript
Rust在前端与全栈开发中的实践探索
随着Rust语言的日渐成熟,其应用场景已经从后端扩展到前端和全栈开发领域。本文将深入探讨Rust语言在前端与全栈开发中的实际应用案例,分析Rust语言在这些领域的优势和面临的挑战,并展望Rust未来的发展趋势。
|
2月前
|
Rust 前端开发 JavaScript
Rust与JavaScript的跨语言交互:探索与实践
本文旨在探讨Rust与JavaScript之间的跨语言交互方法。我们将深入了解WebAssembly(Wasm)的角色,以及它如何使得Rust与JavaScript能够在Web应用中和谐共处。此外,我们还将介绍Rust与JavaScript的集成方式,包括Rust编译到Wasm、使用wasm-bindgen进行Rust与JavaScript的绑定,并通过实际案例展示如何实现两者之间的交互。
|
2月前
|
缓存 Rust 安全
Rust中的RESTful API构建:实践与探索
本文详细阐述了在Rust编程语言中如何构建RESTful API的过程。我们将通过实际示例,介绍Rust的生态系统中用于构建API的流行库和框架,包括Actix-Web、Rocket和Gotham。此外,我们还将讨论RESTful设计原则、API安全性、性能优化等方面的内容,帮助读者在Rust中高效、安全地构建RESTful API。
|
2月前
|
缓存 Rust 算法
Rust中的数据结构与算法优化实践
在Rust编程语言中,优化数据结构与算法是提高程序性能的关键。本文首先介绍了Rust的特点,然后重点讨论了如何在Rust中优化数据结构和算法,包括使用标准库中的高效数据结构、自定义数据结构的优化技巧、算法选择与改进、以及Rust特性如所有权和借用检查器的应用。通过实际案例,我们将展示如何在Rust中实现更高效的数据结构与算法。
|
10月前
|
Rust JavaScript 前端开发
将 Rust 程序编译为 WebAssembly 的知识与实践
本文记叙如何将一个 Rust 语言编译成可执行的 WebAssembly 文件。
165 0
|
开发框架 Rust 监控
QCon 2022·上海站 | 学习笔记3: 字节跳动在 Rust 方向的探索和实践
QCon 2022·上海站 | 学习笔记3: 字节跳动在 Rust 方向的探索和实践
419 0
|
Rust 供应链 安全
QCon 2022·上海站 | 学习笔记2: Rust 程序可靠性保证相关技术探索与实践
QCon 2022·上海站 | 学习笔记2: Rust 程序可靠性保证相关技术探索与实践
188 0
|
缓存 Rust 前端开发
GIAC-2022sh 学习笔记 | QEngine: 支付宝实时行情服务 Rust 跨端实践
GIAC-2022sh 学习笔记 | QEngine: 支付宝实时行情服务 Rust 跨端实践
168 0
GIAC-2022sh 学习笔记 | QEngine: 支付宝实时行情服务 Rust 跨端实践