WebAssembly 在前端的实战应用:图片处理、加密和计算密集型任务

4276 字
21 分钟
WebAssembly 在前端的实战应用:图片处理、加密和计算密集型任务

前言#

WebAssembly(Wasm)是一种低级的二进制指令格式,可以在现代浏览器中以接近原生的速度运行。它不是要取代 JavaScript,而是作为 JavaScript 的补充——当你需要极致性能时,Wasm 是你的秘密武器。

图片处理、加密解密、音视频编解码、物理模拟、大规模数据计算……这些 CPU 密集型任务在纯 JavaScript 中可能慢得让用户等到怀疑人生,但用 Wasm 可以获得 5-50 倍的性能提升。

本文将从 Wasm 基础原理讲起,展示如何用 Rust 和 C++ 编写 Wasm 模块,通过 wasm-bindgen 与 JavaScript 交互,并给出图片处理、加密计算等实际案例及性能对比。

WebAssembly 基础#

Wasm 是什么#

WebAssembly 是一种:

  • 二进制格式:紧凑高效,加载和解析比 JavaScript 快得多
  • 编译目标:C、C++、Rust、Go、Zig 等语言都可以编译到 Wasm
  • 沙箱执行:运行在浏览器的安全沙箱中,与 JavaScript 共享同一个安全模型
  • 跨平台:浏览器、Node.js、Deno、Cloudflare Workers 都支持

Wasm 模块的加载与实例化#

// 方式一:fetch + instantiateStreaming(推荐,流式编译)
const { instance, module } = await WebAssembly.instantiateStreaming(
fetch('/math.wasm'),
{
env: {
// 导入函数:Wasm 模块可以调用的 JS 函数
log: (value) => console.log('From Wasm:', value),
memory: new WebAssembly.Memory({ initial: 256 }) // 256 页 = 16MB
}
}
);
// 调用导出的函数
const result = instance.exports.add(40, 2);
console.log(result); // 42
// 方式二:先编译,后实例化(适合多次实例化同一模块)
const response = await fetch('/math.wasm');
const bytes = await response.arrayBuffer();
const module2 = await WebAssembly.compile(bytes);
// 可以多次实例化同一个 module
const inst1 = await WebAssembly.instantiate(module2, imports);
const inst2 = await WebAssembly.instantiate(module2, imports);

内存模型#

Wasm 使用线性内存(Linear Memory)——一个连续的字节数组,JavaScript 和 Wasm 共享访问:

// 创建共享内存
const memory = new WebAssembly.Memory({
initial: 10, // 初始 10 页(640KB)
maximum: 100, // 最大 100 页(6.4MB)
shared: false // 是否共享(用于多线程)
});
// JavaScript 通过 ArrayBuffer 访问 Wasm 内存
const buffer = new Uint8Array(memory.buffer);
// 写入数据到 Wasm 内存
const data = new TextEncoder().encode('Hello Wasm!');
buffer.set(data, 0); // 从偏移量 0 开始写入
// Wasm 函数可以读取这块内存中的数据
// 然后 JavaScript 可以读取 Wasm 写入的结果

使用 Rust + wasm-bindgen#

Rust 是编写 Wasm 的最佳语言之一——零成本抽象、无 GC、编译产物小、工具链成熟。

项目搭建#

Terminal window
# 安装 wasm-pack(Rust Wasm 打包工具)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 创建项目
cargo init --lib wasm-demo
cd wasm-demo

配置 Cargo.toml

[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
"console",
"ImageData",
"CanvasRenderingContext2d",
"HtmlCanvasElement"
]}
[profile.release]
opt-level = "z" # 优化体积
lto = true # 链接时优化
strip = true # 去除调试信息

基础:导出函数到 JavaScript#

src/lib.rs
use wasm_bindgen::prelude::*;
// 导出到 JS 的函数
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
// 从 JS 导入函数
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
log(&format!("Hello, {}! From Rust/Wasm", name));
}

编译和使用:

Terminal window
# 编译为 web 目标
wasm-pack build --target web --release
# 产出在 pkg/ 目录:
# pkg/wasm_demo_bg.wasm — Wasm 二进制
# pkg/wasm_demo.js — JS 胶水代码
# pkg/wasm_demo.d.ts — TypeScript 类型
// 在前端使用
import init, { add, fibonacci, greet } from './pkg/wasm_demo.js';
async function main() {
await init(); // 初始化 Wasm 模块
console.log(add(1, 2)); // 3
console.log(fibonacci(50)); // 12586269025
greet('World'); // console: "Hello, World! From Rust/Wasm"
}
main();

实战一:图片处理#

图片像素操作是 Wasm 的经典应用场景——大量数值计算,纯 JavaScript 处理大图非常慢。

Rust 端:图片滤镜#

use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
/// 灰度化滤镜
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32;
let g = data[i + 1] as f32;
let b = data[i + 2] as f32;
// 人眼对绿色最敏感的加权公式
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
data[i] = gray;
data[i + 1] = gray;
data[i + 2] = gray;
// alpha 通道不变
}
}
/// 高斯模糊
#[wasm_bindgen]
pub fn gaussian_blur(data: &mut [u8], width: u32, height: u32, radius: u32) {
let w = width as usize;
let h = height as usize;
let r = radius as i32;
// 创建临时缓冲区
let mut temp = data.to_vec();
// 水平方向模糊
for y in 0..h {
for x in 0..w {
let mut r_sum: f32 = 0.0;
let mut g_sum: f32 = 0.0;
let mut b_sum: f32 = 0.0;
let mut count: f32 = 0.0;
for dx in -r..=r {
let nx = x as i32 + dx;
if nx >= 0 && nx < w as i32 {
let idx = (y * w + nx as usize) * 4;
let weight = 1.0 - (dx.abs() as f32 / (r + 1) as f32);
r_sum += data[idx] as f32 * weight;
g_sum += data[idx + 1] as f32 * weight;
b_sum += data[idx + 2] as f32 * weight;
count += weight;
}
}
let idx = (y * w + x) * 4;
temp[idx] = (r_sum / count) as u8;
temp[idx + 1] = (g_sum / count) as u8;
temp[idx + 2] = (b_sum / count) as u8;
}
}
// 垂直方向模糊
for y in 0..h {
for x in 0..w {
let mut r_sum: f32 = 0.0;
let mut g_sum: f32 = 0.0;
let mut b_sum: f32 = 0.0;
let mut count: f32 = 0.0;
for dy in -r..=r {
let ny = y as i32 + dy;
if ny >= 0 && ny < h as i32 {
let idx = (ny as usize * w + x) * 4;
let weight = 1.0 - (dy.abs() as f32 / (r + 1) as f32);
r_sum += temp[idx] as f32 * weight;
g_sum += temp[idx + 1] as f32 * weight;
b_sum += temp[idx + 2] as f32 * weight;
count += weight;
}
}
let idx = (y * w + x) * 4;
data[idx] = (r_sum / count) as u8;
data[idx + 1] = (g_sum / count) as u8;
data[idx + 2] = (b_sum / count) as u8;
}
}
}
/// 亮度/对比度调整
#[wasm_bindgen]
pub fn adjust_brightness_contrast(data: &mut [u8], brightness: f32, contrast: f32) {
let factor = (259.0 * (contrast + 255.0)) / (255.0 * (259.0 - contrast));
for i in (0..data.len()).step_by(4) {
for c in 0..3 {
let mut value = data[i + c] as f32;
// 亮度
value += brightness;
// 对比度
value = factor * (value - 128.0) + 128.0;
// 钳位到 0-255
data[i + c] = value.max(0.0).min(255.0) as u8;
}
}
}
/// 棕褐色(怀旧)滤镜
#[wasm_bindgen]
pub fn sepia(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32;
let g = data[i + 1] as f32;
let b = data[i + 2] as f32;
data[i] = (r * 0.393 + g * 0.769 + b * 0.189).min(255.0) as u8;
data[i + 1] = (r * 0.349 + g * 0.686 + b * 0.168).min(255.0) as u8;
data[i + 2] = (r * 0.272 + g * 0.534 + b * 0.131).min(255.0) as u8;
}
}
/// 边缘检测(Sobel 算子)
#[wasm_bindgen]
pub fn edge_detect(data: &mut [u8], width: u32, height: u32) {
let w = width as usize;
let h = height as usize;
let original = data.to_vec();
// Sobel 核
let gx: [[i32; 3]; 3] = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
let gy: [[i32; 3]; 3] = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for y in 1..h-1 {
for x in 1..w-1 {
let mut sum_gx: i32 = 0;
let mut sum_gy: i32 = 0;
for ky in 0..3 {
for kx in 0..3 {
let px = (y + ky - 1) * w + (x + kx - 1);
let idx = px * 4;
// 使用灰度值
let gray = (original[idx] as i32 + original[idx + 1] as i32 + original[idx + 2] as i32) / 3;
sum_gx += gray * gx[ky][kx];
sum_gy += gray * gy[ky][kx];
}
}
let magnitude = ((sum_gx * sum_gx + sum_gy * sum_gy) as f32).sqrt().min(255.0) as u8;
let idx = (y * w + x) * 4;
data[idx] = magnitude;
data[idx + 1] = magnitude;
data[idx + 2] = magnitude;
}
}
}

JavaScript 端:Canvas 集成#

import init, {
grayscale,
gaussian_blur,
adjust_brightness_contrast,
sepia,
edge_detect
} from './pkg/wasm_demo.js';
await init();
class ImageProcessor {
#canvas;
#ctx;
constructor(canvas) {
this.#canvas = canvas;
this.#ctx = canvas.getContext('2d');
}
async loadImage(src) {
const img = new Image();
img.crossOrigin = 'anonymous';
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = src;
});
this.#canvas.width = img.width;
this.#canvas.height = img.height;
this.#ctx.drawImage(img, 0, 0);
}
#getImageData() {
return this.#ctx.getImageData(0, 0, this.#canvas.width, this.#canvas.height);
}
#putImageData(imageData) {
this.#ctx.putImageData(imageData, 0, 0);
}
applyGrayscale() {
const imageData = this.#getImageData();
const start = performance.now();
grayscale(imageData.data);
const elapsed = performance.now() - start;
console.log(`Grayscale (Wasm): ${elapsed.toFixed(2)}ms`);
this.#putImageData(imageData);
return elapsed;
}
applyBlur(radius = 5) {
const imageData = this.#getImageData();
const start = performance.now();
gaussian_blur(imageData.data, this.#canvas.width, this.#canvas.height, radius);
const elapsed = performance.now() - start;
console.log(`Blur (Wasm): ${elapsed.toFixed(2)}ms`);
this.#putImageData(imageData);
return elapsed;
}
applySepia() {
const imageData = this.#getImageData();
const start = performance.now();
sepia(imageData.data);
const elapsed = performance.now() - start;
console.log(`Sepia (Wasm): ${elapsed.toFixed(2)}ms`);
this.#putImageData(imageData);
return elapsed;
}
applyEdgeDetect() {
const imageData = this.#getImageData();
const start = performance.now();
edge_detect(imageData.data, this.#canvas.width, this.#canvas.height);
const elapsed = performance.now() - start;
console.log(`Edge detect (Wasm): ${elapsed.toFixed(2)}ms`);
this.#putImageData(imageData);
return elapsed;
}
}
// 使用
const canvas = document.getElementById('canvas');
const processor = new ImageProcessor(canvas);
await processor.loadImage('./photo.jpg');
processor.applyGrayscale(); // ~5ms for 4K image
processor.applyBlur(10); // ~50ms for 4K image
processor.applyEdgeDetect(); // ~30ms for 4K image

性能对比:Wasm vs JavaScript#

// JavaScript 版灰度化
function grayscaleJS(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
data[i] = data[i + 1] = data[i + 2] = gray;
}
}
// 性能对比
function benchmark(imageData) {
const copy1 = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width);
const copy2 = new ImageData(new Uint8ClampedArray(imageData.data), imageData.width);
// JavaScript
const jsStart = performance.now();
for (let i = 0; i < 100; i++) {
grayscaleJS(copy1);
}
const jsTime = performance.now() - jsStart;
// Wasm
const wasmStart = performance.now();
for (let i = 0; i < 100; i++) {
grayscale(copy2.data);
}
const wasmTime = performance.now() - wasmStart;
console.log(`JavaScript: ${jsTime.toFixed(1)}ms (100 iterations)`);
console.log(`WebAssembly: ${wasmTime.toFixed(1)}ms (100 iterations)`);
console.log(`Speedup: ${(jsTime / wasmTime).toFixed(1)}x`);
}
// 典型结果(4K 图片,3840x2160):
// JavaScript: 1250.3ms (100 iterations)
// WebAssembly: 180.7ms (100 iterations)
// Speedup: 6.9x

实战二:加密计算#

密码学计算是另一个 Wasm 大显身手的领域。

Rust 端:SHA-256 和 PBKDF2#

use wasm_bindgen::prelude::*;
use std::num::Wrapping;
// SHA-256 常量
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
];
#[wasm_bindgen]
pub fn sha256(input: &[u8]) -> Vec<u8> {
let mut h: [Wrapping<u32>; 8] = [
Wrapping(0x6a09e667), Wrapping(0xbb67ae85),
Wrapping(0x3c6ef372), Wrapping(0xa54ff53a),
Wrapping(0x510e527f), Wrapping(0x9b05688c),
Wrapping(0x1f83d9ab), Wrapping(0x5be0cd19),
];
// 填充消息
let bit_len = (input.len() as u64) * 8;
let mut msg = input.to_vec();
msg.push(0x80);
while (msg.len() % 64) != 56 {
msg.push(0);
}
msg.extend_from_slice(&bit_len.to_be_bytes());
// 处理每个 512-bit 块
for block in msg.chunks(64) {
let mut w = [Wrapping(0u32); 64];
for i in 0..16 {
w[i] = Wrapping(u32::from_be_bytes([
block[i*4], block[i*4+1], block[i*4+2], block[i*4+3]
]));
}
for i in 16..64 {
let s0 = rotr(w[i-15].0, 7) ^ rotr(w[i-15].0, 18) ^ (w[i-15].0 >> 3);
let s1 = rotr(w[i-2].0, 17) ^ rotr(w[i-2].0, 19) ^ (w[i-2].0 >> 10);
w[i] = w[i-16] + Wrapping(s0) + w[i-7] + Wrapping(s1);
}
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h;
for i in 0..64 {
let s1 = Wrapping(rotr(e.0, 6) ^ rotr(e.0, 11) ^ rotr(e.0, 25));
let ch = (e & f) ^ (!e & g);
let temp1 = hh + s1 + ch + Wrapping(K[i]) + w[i];
let s0 = Wrapping(rotr(a.0, 2) ^ rotr(a.0, 13) ^ rotr(a.0, 22));
let maj = (a & b) ^ (a & c) ^ (b & c);
let temp2 = s0 + maj;
hh = g; g = f; f = e; e = d + temp1;
d = c; c = b; b = a; a = temp1 + temp2;
}
h[0] = h[0] + a; h[1] = h[1] + b;
h[2] = h[2] + c; h[3] = h[3] + d;
h[4] = h[4] + e; h[5] = h[5] + f;
h[6] = h[6] + g; h[7] = h[7] + hh;
}
h.iter().flat_map(|x| x.0.to_be_bytes()).collect()
}
fn rotr(x: u32, n: u32) -> u32 {
(x >> n) | (x << (32 - n))
}
/// SHA-256 十六进制输出
#[wasm_bindgen]
pub fn sha256_hex(input: &[u8]) -> String {
sha256(input).iter().map(|b| format!("{:02x}", b)).collect()
}
/// PBKDF2-SHA256
#[wasm_bindgen]
pub fn pbkdf2_sha256(password: &[u8], salt: &[u8], iterations: u32, key_len: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(key_len);
let mut block_num = 1u32;
while result.len() < key_len {
let mut u = hmac_sha256(password, &[salt, &block_num.to_be_bytes()].concat());
let mut block = u.clone();
for _ in 1..iterations {
u = hmac_sha256(password, &u);
for j in 0..32 {
block[j] ^= u[j];
}
}
result.extend_from_slice(&block[..std::cmp::min(32, key_len - result.len())]);
block_num += 1;
}
result
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> Vec<u8> {
let mut k = if key.len() > 64 {
sha256(key)
} else {
key.to_vec()
};
k.resize(64, 0);
let mut ipad = vec![0x36u8; 64];
let mut opad = vec![0x5cu8; 64];
for i in 0..64 {
ipad[i] ^= k[i];
opad[i] ^= k[i];
}
let inner = sha256(&[&ipad[..], message].concat());
sha256(&[&opad[..], &inner[..]].concat())
}

JavaScript 端使用#

import init, { sha256_hex, pbkdf2_sha256 } from './pkg/wasm_demo.js';
await init();
// SHA-256 哈希
const encoder = new TextEncoder();
const hash = sha256_hex(encoder.encode('Hello, World!'));
console.log(hash);
// "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f"
// PBKDF2 密钥派生
const password = encoder.encode('my-secret-password');
const salt = crypto.getRandomValues(new Uint8Array(16));
const derivedKey = pbkdf2_sha256(password, salt, 100000, 32);
console.log('Derived key:', Array.from(derivedKey).map(b => b.toString(16).padStart(2, '0')).join(''));

性能对比:Wasm SHA-256 vs Web Crypto API vs JavaScript#

const data = new Uint8Array(1024 * 1024); // 1MB 随机数据
crypto.getRandomValues(data);
// Wasm SHA-256
async function benchWasm(iterations) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
sha256_hex(data);
}
return performance.now() - start;
}
// Web Crypto API(浏览器原生)
async function benchWebCrypto(iterations) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
await crypto.subtle.digest('SHA-256', data);
}
return performance.now() - start;
}
// JavaScript 纯实现
async function benchJS(iterations) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
sha256JS(data); // 假设有纯 JS 实现
}
return performance.now() - start;
}
// 典型结果(1MB 数据,100 次迭代):
// Web Crypto API: 120ms (最快,硬件加速)
// Wasm SHA-256: 450ms (接近原生)
// JavaScript: 3200ms (最慢)

注意:对于标准加密算法,Web Crypto API 通常比 Wasm 更快,因为它可能使用硬件加速(AES-NI 等)。Wasm 的优势在于自定义加密算法或 Web Crypto 不支持的算法。

实战三:计算密集型任务#

曼德博集合渲染#

#[wasm_bindgen]
pub fn mandelbrot(
output: &mut [u8],
width: u32,
height: u32,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
max_iter: u32,
) {
let w = width as usize;
let h = height as usize;
for py in 0..h {
for px in 0..w {
let x0 = x_min + (px as f64 / w as f64) * (x_max - x_min);
let y0 = y_min + (py as f64 / h as f64) * (y_max - y_min);
let mut x = 0.0f64;
let mut y = 0.0f64;
let mut iter = 0u32;
while x * x + y * y <= 4.0 && iter < max_iter {
let xtemp = x * x - y * y + x0;
y = 2.0 * x * y + y0;
x = xtemp;
iter += 1;
}
let idx = (py * w + px) * 4;
if iter == max_iter {
// 集合内部:黑色
output[idx] = 0;
output[idx + 1] = 0;
output[idx + 2] = 0;
output[idx + 3] = 255;
} else {
// 根据迭代次数着色
let t = iter as f64 / max_iter as f64;
output[idx] = (9.0 * (1.0 - t) * t * t * t * 255.0) as u8;
output[idx + 1] = (15.0 * (1.0 - t) * (1.0 - t) * t * t * 255.0) as u8;
output[idx + 2] = (8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t * 255.0) as u8;
output[idx + 3] = 255;
}
}
}
}
import init, { mandelbrot } from './pkg/wasm_demo.js';
await init();
const canvas = document.getElementById('fractal');
const ctx = canvas.getContext('2d');
const width = 1920;
const height = 1080;
canvas.width = width;
canvas.height = height;
function render(xMin, xMax, yMin, yMax, maxIter) {
const imageData = ctx.createImageData(width, height);
const start = performance.now();
mandelbrot(imageData.data, width, height, xMin, xMax, yMin, yMax, maxIter);
const elapsed = performance.now() - start;
ctx.putImageData(imageData, 0, 0);
console.log(`Rendered in ${elapsed.toFixed(1)}ms`);
return elapsed;
}
// 初始渲染
render(-2.5, 1.0, -1.0, 1.0, 1000);
// 典型结果:~80ms (Wasm) vs ~800ms (JavaScript)
// 支持缩放交互
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const clickX = (e.clientX - rect.left) / rect.width;
const clickY = (e.clientY - rect.top) / rect.height;
// ... 缩放逻辑
});

使用 C++ 编写 Wasm#

如果你的团队更熟悉 C++,也可以使用 Emscripten:

Terminal window
# 安装 Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest
source ./emsdk_env.sh
// matrix.cpp — 矩阵乘法
#include <emscripten/emscripten.h>
#include <cstdlib>
extern "C" {
EMSCRIPTEN_KEEPALIVE
void matrix_multiply(
const float* A, const float* B, float* C,
int M, int N, int K
) {
for (int i = 0; i < M; i++) {
for (int j = 0; j < K; j++) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {
sum += A[i * N + k] * B[k * K + j];
}
C[i * K + j] = sum;
}
}
}
EMSCRIPTEN_KEEPALIVE
float* create_matrix(int rows, int cols) {
return (float*)malloc(rows * cols * sizeof(float));
}
EMSCRIPTEN_KEEPALIVE
void free_matrix(float* ptr) {
free(ptr);
}
}
Terminal window
emcc matrix.cpp -O3 \
-s WASM=1 \
-s EXPORTED_FUNCTIONS='["_matrix_multiply", "_create_matrix", "_free_matrix", "_malloc", "_free"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap", "HEAPF32"]' \
-o matrix.js
// 使用 Emscripten 编译的模块
import createModule from './matrix.js';
const Module = await createModule();
const M = 512, N = 512, K = 512;
// 在 Wasm 内存中分配矩阵
const ptrA = Module._create_matrix(M, N);
const ptrB = Module._create_matrix(N, K);
const ptrC = Module._create_matrix(M, K);
// 填充数据
const heapA = new Float32Array(Module.HEAPF32.buffer, ptrA, M * N);
const heapB = new Float32Array(Module.HEAPF32.buffer, ptrB, N * K);
for (let i = 0; i < M * N; i++) heapA[i] = Math.random();
for (let i = 0; i < N * K; i++) heapB[i] = Math.random();
// 执行矩阵乘法
const start = performance.now();
Module._matrix_multiply(ptrA, ptrB, ptrC, M, N, K);
const elapsed = performance.now() - start;
console.log(`512x512 matrix multiply: ${elapsed.toFixed(1)}ms`);
// 典型结果:~120ms (Wasm) vs ~2500ms (JavaScript)
// 读取结果
const result = new Float32Array(Module.HEAPF32.buffer, ptrC, M * K);
// 释放内存
Module._free_matrix(ptrA);
Module._free_matrix(ptrB);
Module._free_matrix(ptrC);

优化策略与最佳实践#

1. 减少 JS-Wasm 边界跨越#

// ❌ 每个像素都跨越边界
for (let i = 0; i < pixels.length; i++) {
pixels[i] = wasmModule.processPixel(pixels[i]);
}
// ✅ 批量传输,一次处理
const ptr = wasmModule.alloc(pixels.length);
new Uint8Array(wasmModule.memory.buffer, ptr, pixels.length).set(pixels);
wasmModule.processAllPixels(ptr, pixels.length);
const result = new Uint8Array(wasmModule.memory.buffer, ptr, pixels.length);

2. 使用 SharedArrayBuffer + Web Workers 实现多线程#

main.js
const memory = new WebAssembly.Memory({
initial: 256,
maximum: 256,
shared: true
});
const numWorkers = navigator.hardwareConcurrency || 4;
const workers = [];
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker('worker.js');
worker.postMessage({
memory,
module: wasmModule,
startRow: Math.floor(height / numWorkers * i),
endRow: Math.floor(height / numWorkers * (i + 1)),
width, height
});
workers.push(worker);
}
await Promise.all(workers.map(w =>
new Promise(resolve => w.onmessage = resolve)
));

3. Wasm 模块体积优化#

Terminal window
# Rust: 优化体积
wasm-pack build --release
wasm-opt -Oz -o optimized.wasm pkg/wasm_demo_bg.wasm
# 典型体积对比:
# 未优化: 150KB
# release: 45KB
# wasm-opt: 32KB
# + gzip: 12KB

4. 懒加载 Wasm 模块#

// 只在需要时加载 Wasm
class WasmImageProcessor {
#module = null;
async #ensureLoaded() {
if (!this.#module) {
const { default: init, ...exports } = await import('./pkg/wasm_demo.js');
await init();
this.#module = exports;
}
return this.#module;
}
async applyFilter(canvas, filterName) {
const mod = await this.#ensureLoaded();
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
mod[filterName](imageData.data, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
}
}

什么时候该用 Wasm#

场景推荐原因
图片/视频处理✅ Wasm大量像素级数值计算
自定义加密算法✅ WasmCPU 密集型
物理模拟/游戏引擎✅ Wasm实时计算需求
数据压缩/解压✅ Wasm可复用 C 库(zlib 等)
DOM 操作❌ JSWasm 不能直接操作 DOM
网络请求❌ JS异步 I/O 是 JS 强项
标准加密(AES, SHA)❌ Web Crypto硬件加速更快
简单业务逻辑❌ JS开发成本不值得

总结#

WebAssembly 为前端打开了一扇性能之门:

  1. 选对场景:CPU 密集型计算是 Wasm 的主战场,I/O 密集型还是 JavaScript 的天下
  2. 选对语言:Rust(wasm-bindgen)和 C++(Emscripten)是最成熟的选择
  3. 减少边界跨越:批量传输数据,避免频繁的 JS-Wasm 调用
  4. 渐进式引入:通过懒加载和动态导入,只在需要时才加载 Wasm 模块
  5. 性能验证:始终做 benchmark,不是所有场景 Wasm 都比 JS 快(比如 Web Crypto API)

WebAssembly 不是 JavaScript 的替代品,而是它最强大的补充。在正确的场景下使用它,可以为用户带来原生应用级别的体验。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

WebAssembly 在前端的实战应用:图片处理、加密和计算密集型任务
https://boke.hackerdream.xyz/posts/webassembly-frontend/
作者
晴天
发布于
2026-03-20
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录