跳到主要内容

Wasm 实战应用

问题

WebAssembly 在前端有哪些实际应用场景?如何在项目中集成和使用 Wasm?

答案

1. 图片处理 — Squoosh / Sharp

浏览器端图片压缩和格式转换是 Wasm 的经典应用场景。

// 使用 @aspect-build/aspect-wasm-image 做图片压缩(简化示例)
// 实际项目中可使用 squoosh 的 codecs

// 方式 1: 使用现成库 — browser-image-compression
import imageCompression from 'browser-image-compression';

async function compressImage(file: File): Promise<File> {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true, // 在 Worker 中使用 Wasm 避免阻塞主线程
};
return imageCompression(file, options);
}

// 方式 2: 使用 wasm-vips(libvips 的 Wasm 版本)
import Vips from 'wasm-vips';

async function processImage(buffer: ArrayBuffer): Promise<ArrayBuffer> {
const vips = await Vips();

// 从 buffer 加载图片
const image = vips.Image.newFromBuffer(buffer);

// 调整大小 + 转换为 WebP
const processed = image
.resize(0.5) // 缩小 50%
.sharpen() // 锐化
.webpsave({ // 保存为 WebP
Q: 80, // 质量 80
strip: true, // 去除元数据
});

return processed;
}

2. 视频/音频处理 — FFmpeg.wasm

FFmpeg.wasm 将完整的 FFmpeg 编译为 Wasm,在浏览器端做视频处理:

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

async function initFFmpeg(): Promise<FFmpeg> {
const ffmpeg = new FFmpeg();

// 加载 FFmpeg wasm 核心(约 30MB)
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd';
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});

return ffmpeg;
}

// 视频转码:MP4 → GIF
async function videoToGif(videoFile: File): Promise<Blob> {
const ffmpeg = await initFFmpeg();

// 设置进度回调
ffmpeg.on('progress', ({ progress, time }) => {
console.log(`进度: ${(progress * 100).toFixed(1)}%, 时间: ${time}`);
});

// 写入输入文件到虚拟文件系统
await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));

// 执行转码命令
await ffmpeg.exec([
'-i', 'input.mp4',
'-vf', 'fps=10,scale=320:-1:flags=lanczos',
'-t', '5', // 前 5 秒
'output.gif',
]);

// 读取输出文件
const data = await ffmpeg.readFile('output.gif');
return new Blob([data], { type: 'image/gif' });
}

// 提取音频
async function extractAudio(videoFile: File): Promise<Blob> {
const ffmpeg = await initFFmpeg();

await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
await ffmpeg.exec([
'-i', 'input.mp4',
'-vn', // 去掉视频
'-acodec', 'libmp3lame',
'-q:a', '2',
'output.mp3',
]);

const data = await ffmpeg.readFile('output.mp3');
return new Blob([data], { type: 'audio/mpeg' });
}

// 视频截取缩略图
async function generateThumbnail(videoFile: File, timeInSeconds: number): Promise<Blob> {
const ffmpeg = await initFFmpeg();

await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
await ffmpeg.exec([
'-i', 'input.mp4',
'-ss', String(timeInSeconds),
'-frames:v', '1',
'-q:v', '2',
'thumbnail.jpg',
]);

const data = await ffmpeg.readFile('thumbnail.jpg');
return new Blob([data], { type: 'image/jpeg' });
}
FFmpeg.wasm 注意事项
  • 核心文件约 30MB,需要考虑加载策略(预加载/按需加载)
  • 需要 SharedArrayBuffer,要求设置 COOP/COEP Headers
  • 复杂操作应在 Web Worker 中执行,避免阻塞主线程

3. 浏览器端数据库 — sql.js / DuckDB

// sql.js: SQLite 的 Wasm 版本
import initSqlJs, { Database } from 'sql.js';

async function createDB(): Promise<Database> {
const SQL = await initSqlJs({
// 指定 wasm 文件路径
locateFile: (file: string) => `https://sql.js.org/dist/${file}`,
});

const db = new SQL.Database();

// 创建表
db.run(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER
)
`);

// 插入数据
db.run(
'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
['Alice', 'alice@example.com', 25]
);

// 查询
const results = db.exec('SELECT * FROM users WHERE age > ?', [18]);
console.log(results);

return db;
}

// DuckDB-Wasm: 分析型数据库,支持 SQL 分析大数据集
import * as duckdb from '@duckdb/duckdb-wasm';

async function analyticsQuery(): Promise<void> {
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);

const worker = new Worker(bundle.mainWorker!);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);

const conn = await db.connect();

// 直接查询 CSV/Parquet 文件
await conn.query(`
SELECT category, SUM(amount) as total
FROM read_csv_auto('https://example.com/sales.csv')
GROUP BY category
ORDER BY total DESC
LIMIT 10
`);

await conn.close();
}

4. 加密计算

Wasm 在密码学运算(哈希、加解密)中有显著性能优势:

// 使用 argon2-browser(Argon2 密码哈希的 Wasm 实现)
import argon2 from 'argon2-browser';

async function hashPassword(password: string): Promise<string> {
const result = await argon2.hash({
pass: password,
salt: crypto.getRandomValues(new Uint8Array(16)),
type: argon2.ArgonType.Argon2id,
hashLen: 32,
timeCost: 3,
memCost: 65536, // 64MB
parallelism: 4,
});

return result.encoded; // $argon2id$v=19$m=65536,t=3,p=4$...
}

// 使用 hash-wasm(多种哈希算法的 Wasm 实现)
import { sha256, md5, blake3 } from 'hash-wasm';

// 文件哈希计算(秒传检测)
async function calculateFileHash(file: File): Promise<string> {
const buffer = await file.arrayBuffer();
const hash = await sha256(new Uint8Array(buffer));
return hash;
}

// 大文件分片哈希
async function calculateChunkHash(file: File): Promise<string> {
const { createSHA256 } = await import('hash-wasm');
const hasher = await createSHA256();
hasher.init();

const chunkSize = 4 * 1024 * 1024; // 4MB
let offset = 0;

while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
const buffer = await chunk.arrayBuffer();
hasher.update(new Uint8Array(buffer));
offset += chunkSize;
}

return hasher.digest('hex');
}

5. 文本/代码编辑器

许多现代代码编辑器使用 Wasm 实现语法高亮和语言服务:

// Tree-sitter: 增量解析器(Wasm 版)
// 用于代码编辑器的语法高亮和代码分析
import Parser from 'web-tree-sitter';

async function initTreeSitter(): Promise<void> {
await Parser.init();
const parser = new Parser();

// 加载 JavaScript 语法
const JavaScript = await Parser.Language.load('/tree-sitter-javascript.wasm');
parser.setLanguage(JavaScript);

// 解析代码
const tree = parser.parse(`
function greeting(name) {
return \`Hello, \${name}!\`;
}
`);

// 遍历 AST
const rootNode = tree.rootNode;
console.log(rootNode.toString());
// (program (function_declaration name: (identifier) ...))

// 增量更新(只重新解析变化的部分)
tree.edit({
startIndex: 10,
oldEndIndex: 18,
newEndIndex: 15,
startPosition: { row: 0, column: 10 },
oldEndPosition: { row: 0, column: 18 },
newEndPosition: { row: 0, column: 15 },
});

const newTree = parser.parse(newCode, tree); // 增量解析
}

6. Wasm in Web Worker

计算密集型的 Wasm 任务应在 Web Worker 中执行:

worker.ts
// Web Worker 中加载和运行 Wasm
import init, { fibonacci, processData } from './pkg/my_wasm.js';

let initialized = false;

self.onmessage = async (event: MessageEvent) => {
const { type, payload } = event.data;

if (!initialized) {
await init();
initialized = true;
}

switch (type) {
case 'fibonacci': {
const result = fibonacci(payload.n);
self.postMessage({ type: 'result', result });
break;
}
case 'processData': {
// 使用 Transferable 避免拷贝
const input = new Float64Array(payload.buffer);
const output = processData(input);
self.postMessage(
{ type: 'result', buffer: output.buffer },
[output.buffer] // Transfer ownership
);
break;
}
}
};
main.ts
// 主线程中使用 Worker

class WasmWorkerPool {
private workers: Worker[] = [];
private taskQueue: Array<{ resolve: Function; reject: Function; data: unknown }> = [];
private available: Worker[] = [];

constructor(size = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < size; i++) {
const worker = new Worker(new URL('./worker.ts', import.meta.url), {
type: 'module',
});
this.workers.push(worker);
this.available.push(worker);
}
}

async execute<T>(type: string, payload: unknown): Promise<T> {
return new Promise((resolve, reject) => {
const worker = this.available.pop();

if (worker) {
this.runOnWorker(worker, type, payload, resolve, reject);
} else {
// 所有 Worker 正忙,加入队列
this.taskQueue.push({ resolve, reject, data: { type, payload } });
}
});
}

private runOnWorker(
worker: Worker,
type: string,
payload: unknown,
resolve: Function,
reject: Function
): void {
worker.onmessage = (event) => {
resolve(event.data.result);

// Worker 空闲了,检查队列
const next = this.taskQueue.shift();
if (next) {
const { type, payload } = next.data as { type: string; payload: unknown };
this.runOnWorker(worker, type, payload, next.resolve, next.reject);
} else {
this.available.push(worker);
}
};

worker.onerror = (error) => reject(error);
worker.postMessage({ type, payload });
}

terminate(): void {
this.workers.forEach((w) => w.terminate());
}
}

// 使用
const pool = new WasmWorkerPool(4);
const result = await pool.execute('fibonacci', { n: 45 });

7. 性能对比

// 性能基准测试:JS vs Wasm
async function benchmark(): Promise<void> {
// 纯 JS 实现
function fibJS(n: number): number {
if (n <= 1) return n;
return fibJS(n - 1) + fibJS(n - 2);
}

// Wasm 实现(假设已加载)
const { fibonacci: fibWasm } = wasmInstance.exports as {
fibonacci: (n: number) => number;
};

const N = 40;

// JS 测试
console.time('JS fibonacci');
fibJS(N);
console.timeEnd('JS fibonacci');
// JS fibonacci: ~1200ms

// Wasm 测试
console.time('Wasm fibonacci');
fibWasm(N);
console.timeEnd('Wasm fibonacci');
// Wasm fibonacci: ~300ms(快约 4 倍)

// 数组求和(大数据量)
const arr = new Float64Array(10_000_000);
for (let i = 0; i < arr.length; i++) arr[i] = Math.random();

console.time('JS sum');
let jsSum = 0;
for (let i = 0; i < arr.length; i++) jsSum += arr[i];
console.timeEnd('JS sum');
// JS sum: ~15ms(V8 优化后,简单循环差距不大)

console.time('Wasm sum');
// wasmSum(arr.byteOffset, arr.length);
console.timeEnd('Wasm sum');
// Wasm sum: ~12ms(简单操作差距不明显)
}
何时用 Wasm?
  • 适合:递归计算、矩阵运算、图像像素处理、加密哈希、物理模拟、编解码
  • 不适合:DOM 操作、简单业务逻辑、I/O 密集型任务、小数据量操作
  • 判断标准:如果 JS 的执行时间是瓶颈(DevTools Profile 确认),且是 CPU 密集型,考虑 Wasm

8. 项目集成最佳实践


常见面试问题

Q1: 什么场景下应该使用 WebAssembly 而不是 JavaScript?

答案

使用 Wasm 的判断标准:

  1. Performance Profile 确认 CPU 是瓶颈:不是 I/O 或 DOM
  2. 计算密集型:数值计算、像素处理、编解码
  3. 已有非 JS 代码库:C/C++ 库移植(如 FFmpeg、SQLite)
  4. 确定性性能:需要稳定的执行时间(游戏帧率、音视频处理)
  5. 安全计算:密码学运算

不建议使用 Wasm 的场景:

  • 简单的 CRUD 业务逻辑
  • DOM 操作密集的 UI
  • 数据量小、计算简单的函数
  • 团队没有 C/Rust 经验且不值得学习

Q2: Wasm 模块的加载和初始化流程是什么?

答案

// 完整流程
async function loadWasmModule(): Promise<WebAssembly.Instance> {
// 1. 获取 .wasm 文件
const response = await fetch('/module.wasm');

// 2. 流式编译 + 实例化(推荐,边下载边编译)
const { instance, module } = await WebAssembly.instantiateStreaming(
response,
{
env: {
// 导入的 JS 函数(供 Wasm 调用)
memory: new WebAssembly.Memory({ initial: 256 }),
log: (x: number) => console.log(x),
},
}
);

// 3. 缓存编译好的 module(下次秒加载)
const cache = await caches.open('wasm-cache');
// Module 可以序列化缓存

// 4. 使用导出的函数
const exports = instance.exports;
return instance;
}

关键优化:

  • instantiateStreaming 比先 arrayBuffer()instantiate()
  • 缓存编译后的 ModuleWebAssembly.Module 可存到 IndexedDB
  • Service Worker 预缓存:.wasm 文件缓存策略
  • Brotli/Gzip 压缩:.wasm 文件压缩率很高(可达 50-70%)

Q3: SharedArrayBuffer 和 Wasm 有什么关系?

答案

SharedArrayBuffer 使多个线程(Worker)可以共享同一块内存,Wasm 多线程(pthread)依赖它:

// 需要设置 HTTP Headers:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp

const shared = new SharedArrayBuffer(1024);
const view = new Int32Array(shared);

// 主线程和 Worker 可以同时访问同一块内存
// Atomics API 用于同步
Atomics.store(view, 0, 42);
Atomics.wait(view, 0, 42); // 等待值变化

FFmpeg.wasm 等库需要 SharedArrayBuffer 来实现多线程加速。如果无法设置 COOP/COEP headers,需要使用单线程版本。

Q4: 如何优化 Wasm 模块的加载性能?

答案

优化手段说明
流式编译instantiateStreaming 边下载边编译
Brotli 压缩.wasm 压缩率极高
代码分割按功能拆分多个 .wasm 模块
缓存IndexedDB 缓存编译后的 Module
预加载<link rel="preload" href="x.wasm" as="fetch">
wasm-optBinaryen 工具优化体积和性能
延迟加载非关键 Wasm 按需加载
CDN.wasm 文件放 CDN
// 缓存编译后的 Module
async function getCachedModule(url: string): Promise<WebAssembly.Module> {
const cache = await caches.open('wasm-v1');
const cached = await cache.match(url);

if (cached) {
const buffer = await cached.arrayBuffer();
return WebAssembly.compile(buffer);
}

const response = await fetch(url);
const cloned = response.clone();
await cache.put(url, cloned);

return WebAssembly.compileStreaming(response);
}

Q5: AssemblyScript 和 TypeScript 有什么区别?

答案

AssemblyScript 使用 TypeScript 语法,但编译为 Wasm 而非 JS,有重要差异:

维度TypeScriptAssemblyScript
编译目标JavaScriptWebAssembly
类型系统可选类型、any严格类型、无 any
数字类型只有 numberi32, i64, f32, f64
GCJS GC手动/引用计数
标准库完整受限(无 DOM、部分 API)
null 处理可选必须显式处理
字符串原生支持需要额外开销
// AssemblyScript(看起来像 TS,但细节不同)
export function add(a: i32, b: i32): i32 {
return a + b;
}

// 数组需要使用 typed arrays
export function sum(arr: Int32Array): i32 {
let total: i32 = 0;
for (let i = 0; i < arr.length; i++) {
total += unchecked(arr[i]); // unchecked 跳过边界检查
}
return total;
}

相关链接