WebAssembly 入门教程 / 10 - 浏览器应用
10 - 浏览器应用
浏览器是 WebAssembly 的"主场"——在这里,Wasm 与 Web API 深度融合。
10.1 浏览器兼容性
全球支持率(2025 年)
| 特性 | Chrome | Firefox | Safari | Edge | 支持率 |
|---|---|---|---|---|---|
| MVP(基础 Wasm) | ✅ | ✅ | ✅ | ✅ | 97%+ |
| Multi-value | ✅ | ✅ | ✅ | ✅ | 97%+ |
| SIMD | ✅ | ✅ | ✅ | ✅ | 95%+ |
| Threads | ✅ | ✅ | ✅ | ✅ | 90%+ |
| Reference Types | ✅ | ✅ | ✅ | ✅ | 93%+ |
| Tail Call | ✅ | ✅ | ✅ | ✅ | 88%+ |
| GC | ✅ | 🔶 | 🔶 | ✅ | 75%+ |
| Exception Handling | ✅ | ✅ | ✅ | ✅ | 93%+ |
💡 提示:可以使用 caniuse.com 查看最新的浏览器兼容性数据。
10.2 Web Workers 中的 Wasm
基础模式
// main.js
const worker = new Worker('wasm-worker.js', { type: 'module' });
worker.postMessage({
type: 'init',
wasmUrl: '/wasm/image-process.wasm'
});
worker.onmessage = (e) => {
switch (e.data.type) {
case 'ready':
// 发送计算任务
worker.postMessage({
type: 'process',
imageData: getImageData()
});
break;
case 'result':
displayResult(e.data.result);
break;
}
};
// wasm-worker.js
let exports = null;
let memory = null;
self.onmessage = async (e) => {
switch (e.data.type) {
case 'init': {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 })
}
};
const response = await fetch(e.data.wasmUrl);
const { instance } = await WebAssembly.instantiateStreaming(
response, imports
);
exports = instance.exports;
memory = exports.memory;
self.postMessage({ type: 'ready' });
break;
}
case 'process': {
const { imageData } = e.data;
// 将图像数据复制到 Wasm 内存
const ptr = exports.malloc(imageData.byteLength);
const wasmView = new Uint8Array(memory.buffer, ptr, imageData.byteLength);
wasmView.set(new Uint8Array(imageData));
// 执行处理
const resultPtr = exports.process_image(ptr, imageData.width, imageData.height);
// 读取结果
const resultLength = exports.get_result_length();
const result = new Uint8Array(memory.buffer, resultPtr, resultLength).slice();
// 释放内存
exports.free(ptr);
exports.free_result(resultPtr);
self.postMessage({ type: 'result', result: result.buffer }, [result.buffer]);
break;
}
}
};
10.3 SharedArrayBuffer 并行计算
设置正确的 HTTP 头
# Nginx 配置
location / {
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
}
并行图像处理
// parallel-main.js
const WORKER_COUNT = navigator.hardwareConcurrency || 4;
class ParallelProcessor {
constructor(wasmUrl) {
this.sharedMemory = new WebAssembly.Memory({
initial: 1024,
maximum: 2048,
shared: true
});
this.workers = [];
this.barrier = new Int32Array(
new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * WORKER_COUNT)
);
for (let i = 0; i < WORKER_COUNT; i++) {
const worker = new Worker('parallel-worker.js');
worker.postMessage({
type: 'init',
wasmUrl,
memory: this.sharedMemory,
workerId: i,
totalWorkers: WORKER_COUNT
});
this.workers.push(worker);
}
}
async processChunk(imageData, startY, endY) {
return new Promise((resolve) => {
const worker = this.workers[0]; // 或分配到不同 worker
worker.onmessage = (e) => resolve(e.data);
worker.postMessage({
type: 'process',
startY,
endY,
width: imageData.width
});
});
}
}
10.4 Canvas 和 WebGL 集成
Wasm + Canvas 2D
// Rust 侧使用 web-sys 操作 Canvas
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
#[wasm_bindgen]
pub struct CanvasRenderer {
ctx: CanvasRenderingContext2d,
width: u32,
height: u32,
buffer: Vec<u8>,
}
#[wasm_bindgen]
impl CanvasRenderer {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<CanvasRenderer, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap()
.dyn_into::<HtmlCanvasElement>()?;
let ctx = canvas.get_context("2d")?.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
let width = canvas.width();
let height = canvas.height();
let buffer = vec![0u8; (width * height * 4) as usize];
Ok(CanvasRenderer { ctx, width, height, buffer })
}
pub fn render_mandelbrot(&mut self, zoom: f64, center_x: f64, center_y: f64) {
for y in 0..self.height {
for x in 0..self.width {
let cx = (x as f64 - self.width as f64 / 2.0) / zoom + center_x;
let cy = (y as f64 - self.height as f64 / 2.0) / zoom + center_y;
let mut zx = 0.0;
let mut zy = 0.0;
let mut iter = 0;
let max_iter = 255;
while zx * zx + zy * zy < 4.0 && iter < max_iter {
let tmp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tmp;
iter += 1;
}
let idx = ((y * self.width + x) * 4) as usize;
self.buffer[idx] = iter as u8;
self.buffer[idx + 1] = (iter * 2) as u8;
self.buffer[idx + 2] = (iter * 5) as u8;
self.buffer[idx + 3] = 255;
}
}
}
pub fn flush(&self) -> Result<(), JsValue> {
let data = ImageData::new_with_u8_clamped_array(
wasm_bindgen::Clamped(&self.buffer),
self.width,
)?;
self.ctx.put_image_data(&data, 0.0, 0.0)?;
Ok(())
}
}
Wasm + WebGL
use wasm_bindgen::prelude::*;
use web_sys::{WebGl2RenderingContext, WebGlProgram, WebGlShader};
#[wasm_bindgen]
pub struct WebGLRenderer {
gl: WebGl2RenderingContext,
program: WebGlProgram,
}
#[wasm_bindgen]
impl WebGLRenderer {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<WebGLRenderer, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document.get_element_by_id(canvas_id).unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()?;
let gl = canvas.get_context("webgl2")?.unwrap()
.dyn_into::<WebGl2RenderingContext>()?;
// 编译着色器
let vert_shader = compile_shader(
&gl,
WebGl2RenderingContext::VERTEX_SHADER,
r#"#version 300 es
in vec4 a_position;
void main() {
gl_Position = a_position;
}"#,
)?;
let frag_shader = compile_shader(
&gl,
WebGl2RenderingContext::FRAGMENT_SHADER,
r#"#version 300 es
precision highp float;
out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}"#,
)?;
let program = link_program(&gl, &vert_shader, &frag_shader)?;
gl.use_program(Some(&program));
Ok(WebGLRenderer { gl, program })
}
pub fn draw_triangle(&self) {
let vertices: [f32; 9] = [
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5,
];
// 设置顶点缓冲...
}
}
10.5 文件 API 集成
// 用户选择文件后在 Wasm 中处理
async function processFileWithWasm(file) {
const buffer = await file.arrayBuffer();
const imports = { env: { memory: new WebAssembly.Memory({ initial: 256 }) } };
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/wasm/file-processor.wasm'), imports
);
const { memory, process_file, malloc, free, get_error } = instance.exports;
// 复制文件数据到 Wasm 内存
const fileData = new Uint8Array(buffer);
const ptr = malloc(fileData.length);
new Uint8Array(memory.buffer, ptr, fileData.length).set(fileData);
// 处理
const resultPtr = process_file(ptr, fileData.length);
if (resultPtr === 0) {
const errorPtr = get_error();
const error = readWasmString(memory, errorPtr);
throw new Error(error);
}
// 读取结果
const resultLength = instance.exports.get_result_length();
const result = new Uint8Array(memory.buffer, resultPtr, resultLength).slice();
free(ptr);
free(resultPtr);
return result;
}
function readWasmString(memory, ptr) {
const view = new Uint8Array(memory.buffer);
let end = ptr;
while (view[end] !== 0) end++;
return new TextDecoder().decode(view.slice(ptr, end));
}
10.6 Web Audio 中的 Wasm
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct AudioProcessor {
sample_rate: f32,
phase: f32,
frequency: f32,
}
#[wasm_bindgen]
impl AudioProcessor {
#[wasm_bindgen(constructor)]
pub fn new(sample_rate: f32) -> AudioProcessor {
AudioProcessor {
sample_rate,
phase: 0.0,
frequency: 440.0,
}
}
pub fn set_frequency(&mut self, freq: f32) {
self.frequency = freq;
}
// AudioWorklet 调用此方法生成音频样本
pub fn process(&mut self, output_ptr: *mut f32, frame_count: usize) {
let output = unsafe {
std::slice::from_raw_parts_mut(output_ptr, frame_count)
};
let phase_inc = self.frequency / self.sample_rate;
for i in 0..frame_count {
// 正弦波生成
output[i] = (self.phase * 2.0 * std::f32::consts::PI).sin();
self.phase = (self.phase + phase_inc) % 1.0;
}
}
}
// AudioWorkletProcessor 中使用 Wasm
class WasmAudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.wasmReady = false;
this.port.onmessage = (e) => this.initWasm(e.data);
}
async initWasm(wasmBytes) {
const { instance } = await WebAssembly.instantiate(wasmBytes);
this.processor = new instance.exports.AudioProcessor(sampleRate);
this.wasmReady = true;
}
process(inputs, outputs, parameters) {
if (!this.wasmReady) return true;
const output = outputs[0][0];
const ptr = this.malloc(output.length * 4);
this.processor.process(ptr, output.length);
const result = new Float32Array(this.memory.buffer, ptr, output.length);
output.set(result);
this.free(ptr);
return true;
}
}
10.7 Service Worker 离线使用
// service-worker.js
const WASM_CACHE = 'wasm-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(WASM_CACHE).then((cache) => {
return cache.addAll([
'/wasm/app.wasm',
'/wasm/loader.js'
]);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.url.endsWith('.wasm')) {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
// 从缓存加载,流式编译
return response;
}
return fetch(event.request).then((response) => {
const clone = response.clone();
caches.open(WASM_CACHE).then((cache) => {
cache.put(event.request, clone);
});
return response;
});
})
);
}
});
10.8 Wasm + 机器学习(TensorFlow.js)
// 使用 TensorFlow.js 的 Wasm 后端
import '@tensorflow/tfjs-backend-wasm';
import * as tf from '@tensorflow/tfjs';
async function initTF() {
// 设置 Wasm 后端
await tf.setBackend('wasm');
await tf.ready();
console.log('Backend:', tf.getBackend()); // "wasm"
// 运行模型推理
const model = await tf.loadLayersModel('/model/model.json');
const input = tf.tensor2d([[1, 2, 3, 4]]);
const output = model.predict(input);
output.print();
}
10.9 调试技巧
Chrome DevTools
1. 打开 chrome://flags/#enable-devtools-experiments
2. 启用 "WebAssembly Debugging: Enable DWARF support"
3. 编译时保留调试信息:
- C/C++: emcc -g4 --source-map-base http://localhost:8080/
- Rust: 在 Cargo.toml 中设置 debug = true
4. Sources 面板中可以直接查看源码、设断点、查看变量
性能分析
// 使用 Performance API
performance.mark('wasm-start');
instance.exports.heavy_compute();
performance.mark('wasm-end');
performance.measure('wasm-compute', 'wasm-start', 'wasm-end');
const measure = performance.getEntriesByName('wasm-compute')[0];
console.log(`Wasm compute: ${measure.duration}ms`);
Console 断点
// 在 Rust 中插入调试断点
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
#[wasm_bindgen]
pub fn debug_function(x: i32) -> i32 {
console_log!("debug_function called with x = {}", x);
let result = x * 2;
console_log!("result = {}", result);
result
}
10.10 注意事项
⚠️ MIME 类型:务必确保服务器为
.wasm文件返回Content-Type: application/wasm,否则流式编译会失败。
⚠️ CORS:跨域加载
.wasm文件需要正确的 CORS 头。
⚠️ SharedArrayBuffer 限制:需要
COOP和COEP头。如果没有这些头,可以回退到postMessage传递数据。
⚠️ Safari 限制:Safari 对 SharedArrayBuffer 的支持需要用户在设置中启用"Experimental Features"。生产环境应做好降级方案。
10.11 扩展阅读
- WebAssembly for Producers (Chrome)
- Web Workers API (MDN)
- SharedArrayBuffer (MDN)
- TensorFlow.js Wasm Backend
下一章:11 - 边缘计算 — 将 WebAssembly 部署到边缘节点。