WebAssembly(Wasm)与 JavaScript 的关系,最像“高性能

WebAssembly(Wasm)与 JavaScript 的关系,最像“高性能模块 + 胶水与UI”的分工协作。Wasm 提供接近本地的算力,JS 负责加载、绑定、事件、DOM 与生态集成。核心协同点如下:

一、各自角色

  • JavaScript

    • 宿主与编排:加载 Wasm、传参、接收返回值、处理事件与 DOM。

    • 生态桥:与框架、网络、存储、UI 库对接。

  • WebAssembly

    • 算力密集任务:数值计算、编解码、图像/音视频处理、加密、游戏物理。

    • 可被多语言编译产出(C/C++/Rust/Go/…),在浏览器和很多非浏览器环境运行。

二、如何互相调用

  • JS 调 Wasm

    • 通过 WebAssembly.instantiate(Streaming) 加载 .wasm,得到 exports,直接调用导出函数。
  • Wasm 调 JS

    • 通过 importObject 把 JS 函数作为“导入”提供给 Wasm,在 Wasm 内调用这些函数(称为 host functions)。

三、内存与数据交换

  • 共享线性内存:Wasm 有一块 ArrayBuffer(WebAssembly.Memory),JS 可通过 TypedArray 视图读写它。

  • 简单标量直传:i32、i64(部分环境)、f32、f64 可作为参数/返回值直接传递。

  • 复杂数据需编码:

    • 文本:UTF-8 编/解码,把字符串拷贝进 Wasm 内存,并传递指针+长度。

    • 结构体/数组:在 Wasm 侧定义布局,JS 按偏移写入。

  • 工具链简化:

    • Emscripten(C/C++)提供 ccall/cwrap、FS、UTF8 编码工具,自动生成 JS 绑定。

    • Rust 的 wasm-bindgen + wasm-pack 可自动生成类型转换与 JS 绑定,支持直接传/返字符串、数组,甚至与 Web API 交互。

四、加载与初始化路径(最简示例)

  
// 假设有 add(i32,i32)->i32 的导出
  
const imports = { env: { log: (x) => console.log('Wasm says:', x) } };
  

  
const { instance } = await WebAssembly.instantiateStreaming(fetch('/module.wasm'), imports);
  
// 若服务器不带正确 MIME,可用 fetch -> arrayBuffer -> instantiate
  
const { add } = instance.exports;
  
console.log(add(2, 40)); // 42
  

五、字符串传递(手动编码示意)

  
// Wasm 导出 alloc(len) 和 process(ptr,len),并在同一 Memory 上
  
const { memory, alloc, process } = instance.exports;
  
const enc = new TextEncoder(), dec = new TextDecoder();
  

  
function callProcess(str) {
  
  const bytes = enc.encode(str);
  
  const ptr = alloc(bytes.length);
  
  new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
  
  const outPtr = process(ptr, bytes.length); // 假设返回新串指针,长度另有方式返回
  
  // 实务中常用返回值结构体或全局“out_len”
  
}
  

六、Rust 生态的“零样板”式协作

  • Cargo.toml + wasm-bindgen 注解后,JS 可像调用普通函数一样:

    • Rust

      
      use wasm_bindgen::prelude::*;
      #[wasm_bindgen]
      pub fn greet(name: &str) -> String {
        format!("Hello, {name}!")
      }
      
    • JS

      
      import init, { greet } from './pkg/my_wasm.js';
      await init(); // 加载 .wasm
      console.log(greet('Wasm'));
      
  • wasm-bindgen 还能让 Rust 直接调用 Web API(如 window.fetch、Canvas、WebGL)而无需手写 importObject。

七、与 Web API 协同的常见模式

  • 数据在 Wasm 中算,结果在 JS 中渲染 DOM/Canvas/Three.js。

  • 使用 OffscreenCanvas 或 Web Workers:

    • 把 Wasm 放进 Worker,主线程保持流畅 UI。

    • 通过 postMessage 传输 ArrayBuffer(零拷贝转移)以降低传输成本。

八、性能要点与坑

  • 调用边界成本:JS↔Wasm 的函数频繁往返有开销。策略是“批量化”:用少量调用处理大块数据。

  • 拷贝与编码成本:字符串和对象序列化/拷贝昂贵,能用共享内存(TypedArray)就用共享内存。

  • 内存增长:Wasm Memory 扩容会导致 buffer 失效,需要重新获取视图。

  • 异常与调试:Wasm 不直接抛 JS 异常;用工具链提供的调试符号、console/log 桥。

  • GC 集成:目前 Wasm 无内建 GC(有提案进展),与 JS 对象互操作需要绑定工具或手动管理生命周期。

九、什么时候该引入 Wasm

  • 是:热点循环、DSP、压缩/解压、图像滤镜、向量/矩阵、密码学、PDF/字体解析、小游戏引擎核心。

  • 否:主要是业务逻辑、DOM 编排、网络与状态管理,JS/TS 更合适。

十、端到端的协作范式

  • JS 负责:加载/初始化、事件与 UI、调度、与平台 API 对接、把数据喂给 Wasm。

  • Wasm 负责:计算密集核,暴露少量、稳定的函数接口。

  • 数据通路:大块二进制经 TypedArray;高层对象在边界序列化;减少往返次数;必要时用 Worker。