强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

10 - 浏览器应用

10 - 浏览器应用

浏览器是 WebAssembly 的"主场"——在这里,Wasm 与 Web API 深度融合。


10.1 浏览器兼容性

全球支持率(2025 年)

特性ChromeFirefoxSafariEdge支持率
MVP(基础 Wasm)97%+
Multi-value97%+
SIMD95%+
Threads90%+
Reference Types93%+
Tail Call88%+
GC🔶🔶75%+
Exception Handling93%+

💡 提示:可以使用 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 限制:需要 COOPCOEP 头。如果没有这些头,可以回退到 postMessage 传递数据。

⚠️ Safari 限制:Safari 对 SharedArrayBuffer 的支持需要用户在设置中启用"Experimental Features"。生产环境应做好降级方案。


10.11 扩展阅读


下一章11 - 边缘计算 — 将 WebAssembly 部署到边缘节点。