WebAssembly 简介
什么是 WebAssembly?
WebAssembly(Wasm)是一种二进制指令格式,设计用于在 Web 浏览器中以接近原生速度运行代码。它是 W3C 的官方 Web 标准,被所有现代浏览器支持(Chrome、Firefox、Safari、Edge)。
Wasm 不是一种编程语言——你不直接写 Wasm,而是将其他语言(Rust、C++、Go、AssemblyScript 等)编译成 Wasm 格式。Rust 是目前 Wasm 生态中支持最完善、工具链最成熟的语言。
环境搭建
安装工具链
# 1. 添加 Wasm 编译目标
rustup target add wasm32-unknown-unknown
# 2. 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 或者通过 cargo 安装
cargo install wasm-pack
# 验证安装
wasm-pack --version
创建 Wasm 项目
# 创建库项目(Wasm 项目必须是 lib)
cargo new --lib my-wasm-lib
cd my-wasm-lib
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
# cdylib:生成动态链接库,用于 Wasm
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "s" # 优化体积
lto = true
wasm-bindgen:连接 Rust 与 JavaScript
基本用法
#[wasm_bindgen] 宏标记需要暴露给 JavaScript 的函数和类型,wasm-bindgen 工具会自动生成 JS 胶水代码,处理类型转换、内存管理等细节。
// src/lib.rs
use wasm_bindgen::prelude::*;
// 导入 JavaScript 的 console.log
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// 宏简化 console.log 调用
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
// 暴露给 JavaScript 的函数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! 来自 Rust 🦀", name)
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
// 暴露结构体和方法
#[wasm_bindgen]
pub struct Counter {
count: i32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Counter {
Counter { count: 0 }
}
pub fn increment(&mut self) {
self.count += 1;
}
#[wasm_bindgen(getter)]
pub fn value(&self) -> i32 {
self.count
}
}
编译与打包
# 编译为 Wasm 并生成 JS 绑定
# --target web:生成用于浏览器的 ES 模块
wasm-pack build --target web --release
# 生成的文件:
# pkg/
# ├── my_wasm_lib_bg.wasm ← 编译好的 Wasm 二进制
# ├── my_wasm_lib.js ← 自动生成的 JS 胶水代码
# ├── my_wasm_lib.d.ts ← TypeScript 类型声明
# └── package.json
# 其他目标:
wasm-pack build --target nodejs # Node.js CommonJS 模块
wasm-pack build --target bundler # Webpack/Vite 等打包器
在浏览器中调用
HTML 中使用 Wasm 模块
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Rust + Wasm 示例</title>
</head>
<body>
<input id="name" placeholder="输入名字">
<button id="greet-btn">打招呼</button>
<p id="output"></p>
<p>Fibonacci(10) = <span id="fib"></span></p>
<script type="module">
import init, { greet, fibonacci, Counter }
from './pkg/my_wasm_lib.js';
async function main() {
// 初始化 Wasm 模块(异步加载 .wasm 文件)
await init();
// 调用 Rust 函数
document.getElementById('fib').textContent = fibonacci(10);
document.getElementById('greet-btn').onclick = () => {
const name = document.getElementById('name').value;
const msg = greet(name);
document.getElementById('output').textContent = msg;
};
// 使用 Rust 结构体
const counter = new Counter();
counter.increment();
counter.increment();
console.log('计数:', counter.value); // 2
}
main();
</script>
</body>
</html>
操作 DOM:web-sys
从 Rust 直接操作浏览器 API
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = [
"Window",
"Document",
"Element",
"HtmlElement",
"Node",
"console",
]}
use wasm_bindgen::prelude::*;
use web_sys;
#[wasm_bindgen(start)] // Wasm 模块加载时自动调用
pub fn run() -> Result<(), JsValue> {
// 获取 window 和 document
let window = web_sys::window().expect("no global window");
let document = window.document().expect("no document");
// 创建元素
let p = document.create_element("p")?;
p.set_text_content(Some("Hello from Rust! 🦀"));
// 添加到 body
let body = document.body().expect("no body");
body.append_child(&p)?;
Ok(())
}
性能对比
Rust/Wasm vs JavaScript:何时选择 Wasm?
| 场景 | JavaScript | Rust/Wasm | 推荐 |
|---|---|---|---|
| DOM 操作 | 原生,极快 | 需要跨边界调用 | JS |
| 计算密集型(图像处理、密码学、物理模拟) | 受 JIT 限制 | 接近原生速度,2-10x 加速 | Wasm |
| 代码复用(已有 Rust 库) | 需要重写 | 直接复用 | Wasm |
| 初始加载时间 | 快(无需异步初始化) | 需要下载并初始化 .wasm | JS |
| 调试友好度 | 优秀(浏览器 DevTools) | 有限(逐步改善中) | JS |
实战案例:图像滤镜
use wasm_bindgen::prelude::*;
// 对图像像素数据应用灰度滤镜
// 这种像素级操作是 Wasm 相对 JS 优势最明显的场景
#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
// ImageData 的格式:[R, G, B, A, R, G, B, A, ...]
for chunk in pixels.chunks_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
// ITU-R BT.709 亮度权重
let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// chunk[3] 是 alpha 通道,保持不变
}
}
// 添加噪点效果
#[wasm_bindgen]
pub fn invert(pixels: &mut [u8]) {
for chunk in pixels.chunks_mut(4) {
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
<!-- 在 JS 中调用 Rust 图像处理函数 -->
<script type="module">
import init, { grayscale, invert } from './pkg/my_wasm_lib.js';
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 将像素数据传入 Rust 处理(零拷贝:直接传递 SharedArrayBuffer)
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
</script>
项目:Conway 生命游戏
经典 Wasm 示例
Conway 生命游戏是展示 Rust/Wasm 能力的经典示例——大量规则重复的格子运算正是 Wasm 的强项:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<u8>, // 0 = 死,1 = 活
}
#[wasm_bindgen]
impl Universe {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> Universe {
let cells = (0..width * height)
.map(|i| if i % 2 == 0 || i % 7 == 0 { 1 } else { 0 })
.collect();
Universe { width, height, cells }
}
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = (row * self.width + col) as usize;
let live = self.live_neighbor_count(row, col);
next[idx] = match (self.cells[idx], live) {
(1, 2) | (1, 3) => 1, // 存活
(0, 3) => 1, // 诞生
_ => 0, // 死亡
};
}
}
self.cells = next;
}
fn live_neighbor_count(&self, row: u32, col: u32) -> u8 {
let mut count = 0;
for dr in [self.height - 1, 0, 1] {
for dc in [self.width - 1, 0, 1] {
if dr == 0 && dc == 0 { continue; }
let r = (row + dr) % self.height;
let c = (col + dc) % self.width;
count += self.cells[(r * self.width + c) as usize];
}
}
count
}
pub fn cells(&self) -> *const u8 {
self.cells.as_ptr() // 返回指针,JS 通过 Wasm 内存直接读取(零拷贝)
}
pub fn width(&self) -> u32 { self.width }
pub fn height(&self) -> u32 { self.height }
}
Rust/Wasm 和 JavaScript 共享同一块线性内存(WebAssembly.Memory)。通过返回 Rust 数组的原始指针,JavaScript 可以用 new Uint8Array(wasm.memory.buffer, ptr, len) 直接读取数据,而不需要复制——这是高性能 Wasm 应用的关键优化技术。
值得关注的 Wasm 相关 crate:
Leptos / Yew:基于 Wasm 的 Rust 前端框架,类似 React,用 Rust 写前端组件。
Dioxus:跨平台 UI 框架,同一套代码可以运行在 Web(Wasm)、桌面(Tauri)、移动端。
Trunk:Rust/Wasm 前端项目的打包工具,类似 Vite。
gloo:Web API 的 Rust 友好封装集合(事件处理、Timer、Fetch 等)。