Skip to content

实战项目

> 通过图像处理 Web 应用实战掌握 WASM 开发。

项目概述

我们将实现一个图像处理 Web 应用,对比 WebAssembly 和 JavaScript 的性能差异。

┌─────────────────────────────────────────┐
│        图像处理应用架构                   │
├─────────────────────────────────────────┤
│                                         │
│  前端(HTML + JavaScript)               │
│  ┌──────────────────────────────────┐ │
│  │  文件选择                         │ │
│  │  处理选项                         │ │
│  │  性能对比显示                     │ │
│  └──────────────────────────────────┘ │
│                                         │
│  WebAssembly(Rust)                    │
│  ┌──────────────────────────────────┐ │
│  │  图像滤镜                         │ │
│  │  缩放/旋转                        │ │
│  │  批量处理                         │ │
│  └──────────────────────────────────┘ │
│                                         │
│  性能测试                                │
│  ┌──────────────────────────────────┐ │
│  │  WASM vs JavaScript              │ │
│  │  实时对比                         │ │
│  └──────────────────────────────────┘ │
│                                         │
└─────────────────────────────────────────┘

项目创建

1. 创建 WASM 项目

bash
# 创建项目
cargo new --lib image-wasm
cd image-wasm

2. 配置依赖

toml
# Cargo.toml
[package]
name = "image-wasm"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
    "ImageData",
    "CanvasRenderingContext2d",
    "HtmlCanvasElement",
    "HtmlImageElement",
] }

[profile.release]
opt-level = 3
lto = true

3. WASM 实现

rust
// src/lib.rs
use wasm_bindgen::prelude::*;
use js_sys::Uint8ClampedArray;
use web_sys::ImageData;

// 灰度滤镜
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
    for chunk in data.chunks_exact_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        
        // 灰度公式
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
    }
}

// 反色滤镜
#[wasm_bindgen]
pub fn invert(data: &mut [u8]) {
    for chunk in data.chunks_exact_mut(4) {
        chunk[0] = 255 - chunk[0];  // R
        chunk[1] = 255 - chunk[1];  // G
        chunk[2] = 255 - chunk[2];  // B
    }
}

// 亮度调整
#[wasm_bindgen]
pub fn adjust_brightness(data: &mut [u8], factor: f32) {
    for chunk in data.chunks_exact_mut(4) {
        chunk[0] = ((chunk[0] as f32 * factor).min(255.0)) as u8;
        chunk[1] = ((chunk[1] as f32 * factor).min(255.0)) as u8;
        chunk[2] = ((chunk[2] as f32 * factor).min(255.0)) as u8;
    }
}

// 对比度调整
#[wasm_bindgen]
pub fn adjust_contrast(data: &mut [u8], factor: f32) {
    for chunk in data.chunks_exact_mut(4) {
        chunk[0] = (((chunk[0] as f32 - 128.0) * factor + 128.0).clamp(0.0, 255.0)) as u8;
        chunk[1] = (((chunk[1] as f32 - 128.0) * factor + 128.0).clamp(0.0, 255.0)) as u8;
        chunk[2] = (((chunk[2] as f32 - 128.0) * factor + 128.0).clamp(0.0, 255.0)) as u8;
    }
}

// 模糊滤镜(简化版)
#[wasm_bindgen]
pub fn blur(data: &[u8], width: u32, height: u32, radius: u32) -> Vec<u8> {
    let mut result = vec![0u8; data.len()];
    
    for y in 0..height {
        for x in 0..width {
            let mut r_sum = 0.0;
            let mut g_sum = 0.0;
            let mut b_sum = 0.0;
            let mut count = 0.0;
            
            // 计算周围像素的平均值
            for dy in -radius..=radius {
                for dx in -radius..=radius {
                    let ny = (y as i32 + dy).clamp(0, height as i32 - 1) as u32;
                    let nx = (x as i32 + dx).clamp(0, width as i32 - 1) as u32;
                    
                    let idx = (ny * width + nx) * 4;
                    r_sum += data[idx] as f32;
                    g_sum += data[idx + 1] as f32;
                    b_sum += data[idx + 2] as f32;
                    count += 1.0;
                }
            }
            
            let idx = (y * width + x) * 4;
            result[idx] = (r_sum / count) as u8;
            result[idx + 1] = (g_sum / count) as u8;
            result[idx + 2] = (b_sum / count) as u8;
            result[idx + 3] = data[idx + 3];  // Alpha 不变
        }
    }
    
    result
}
▶ Run

4. 构建 WASM

bash
wasm-pack build --release --target web

Web 前端

1. 创建 HTML

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Processor WASM</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        
        .container {
            max-width: 800px;
            margin: auto;
        }
        
        canvas {
            border: 1px solid #ccc;
            margin: 10px;
        }
        
        .controls {
            margin: 20px 0;
        }
        
        button {
            padding: 10px 20px;
            margin: 5px;
        }
        
        .stats {
            margin: 20px 0;
            padding: 10px;
            background: #f0f0f0;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Image Processor (WASM vs JS)</h1>
        
        <input type="file" id="imageInput" accept="image/*">
        
        <div class="controls">
            <button id="grayscaleBtn">Grayscale</button>
            <button id="invertBtn">Invert</button>
            <button id="brightnessBtn">Brightness +20%</button>
            <button id="contrastBtn">Contrast +50%</button>
            <button id="blurBtn">Blur</button>
            <button id="resetBtn">Reset</button>
        </div>
        
        <div>
            <h3>Original</h3>
            <canvas id="originalCanvas"></canvas>
        </div>
        
        <div>
            <h3>Processed (WASM)</h3>
            <canvas id="wasmCanvas"></canvas>
        </div>
        
        <div>
            <h3>Processed (JavaScript)</h3>
            <canvas id="jsCanvas"></canvas>
        </div>
        
        <div class="stats" id="stats">
            Performance comparison will appear here
        </div>
    </div>
    
    <script src="./index.js" type="module"></script>
</body>
</html>

2. JavaScript 实现

javascript
// index.js
import init, {
    grayscale as grayscaleWASM,
    invert as invertWASM,
    adjust_brightness as brightnessWASM,
    adjust_contrast as contrastWASM,
    blur as blurWASM,
} from './pkg/image_wasm.js';

let wasmModule;
let originalImageData;
let canvasWidth, canvasHeight;

// 初始化 WASM
async function initWASM() {
    wasmModule = await init();
    console.log('WASM initialized');
}

// 加载图像
function loadImage(file) {
    const reader = new FileReader();
    reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
            // 设置 canvas
            const originalCanvas = document.getElementById('originalCanvas');
            const wasmCanvas = document.getElementById('wasmCanvas');
            const jsCanvas = document.getElementById('jsCanvas');
            
            canvasWidth = img.width;
            canvasHeight = img.height;
            
            originalCanvas.width = canvasWidth;
            originalCanvas.height = canvasHeight;
            wasmCanvas.width = canvasWidth;
            wasmCanvas.height = canvasHeight;
            jsCanvas.width = canvasWidth;
            jsCanvas.height = canvasHeight;
            
            // 绘制原图
            const ctx = originalCanvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            
            // 保存原始数据
            originalImageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
            
            // 复制到其他 canvas
            wasmCanvas.getContext('2d').putImageData(originalImageData, 0, 0);
            jsCanvas.getContext('2d').putImageData(originalImageData, 0, 0);
        };
        img.src = e.target.result;
    };
    reader.readAsDataURL(file);
}

// WASM 处理
function processWASM(filter, ...args) {
    const canvas = document.getElementById('wasmCanvas');
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
    
    const start = performance.now();
    
    if (filter === 'blur') {
        const result = blurWASM(imageData.data, canvasWidth, canvasHeight, args[0]);
        imageData.data.set(result);
    } else {
        const data = new Uint8Array(imageData.data.buffer);
        filter(data, ...args);
    }
    
    const time = performance.now() - start;
    
    ctx.putImageData(imageData, 0, 0);
    return time;
}

// JavaScript 处理
function grayscaleJS(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] = gray;
        data[i+1] = gray;
        data[i+2] = gray;
    }
}

function invertJS(data) {
    for (let i = 0; i < data.length; i += 4) {
        data[i] = 255 - data[i];
        data[i+1] = 255 - data[i+1];
        data[i+2] = 255 - data[i+2];
    }
}

function brightnessJS(data, factor) {
    for (let i = 0; i < data.length; i += 4) {
        data[i] = Math.min(255, data[i] * factor);
        data[i+1] = Math.min(255, data[i+1] * factor);
        data[i+2] = Math.min(255, data[i+2] * factor);
    }
}

function processJS(filter, ...args) {
    const canvas = document.getElementById('jsCanvas');
    const ctx = canvas.getContext('2d');
    const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
    
    const start = performance.now();
    filter(imageData.data, ...args);
    const time = performance.now() - start;
    
    ctx.putImageData(imageData, 0, 0);
    return time;
}

// 性能对比
function comparePerformance(wasmTime, jsTime) {
    const stats = document.getElementById('stats');
    const speedup = jsTime / wasmTime;
    
    stats.innerHTML = `
        WASM: ${wasmTime.toFixed(2)}ms<br>
        JavaScript: ${jsTime.toFixed(2)}ms<br>
        Speedup: ${speedup.toFixed(2)}x faster
    `;
}

// 处理并对比
function processAndCompare(wasmFilter, jsFilter, ...args) {
    // 重置图像
    resetImages();
    
    const wasmTime = processWASM(wasmFilter, ...args);
    const jsTime = processJS(jsFilter, ...args);
    
    comparePerformance(wasmTime, jsTime);
}

// 重置图像
function resetImages() {
    const wasmCanvas = document.getElementById('wasmCanvas');
    const jsCanvas = document.getElementById('jsCanvas');
    
    wasmCanvas.getContext('2d').putImageData(originalImageData, 0, 0);
    jsCanvas.getContext('2d').putImageData(originalImageData, 0, 0);
}

// 主函数
async function main() {
    await initWASM();
    
    // 文件输入
    document.getElementById('imageInput').addEventListener('change', (e) => {
        loadImage(e.target.files[0]);
    });
    
    // 按钮事件
    document.getElementById('grayscaleBtn').addEventListener('click', () => {
        processAndCompare(grayscaleWASM, grayscaleJS);
    });
    
    document.getElementById('invertBtn').addEventListener('click', () => {
        processAndCompare(invertWASM, invertJS);
    });
    
    document.getElementById('brightnessBtn').addEventListener('click', () => {
        processAndCompare(brightnessWASM, brightnessJS, 1.2);
    });
    
    document.getElementById('contrastBtn').addEventListener('click', () => {
        processAndCompare(contrastWASM, contrastJS, 1.5);
    });
    
    document.getElementById('blurBtn').addEventListener('click', () => {
        processAndCompare(blurWASM, blurJS, 3);
    });
    
    document.getElementById('resetBtn').addEventListener('click', resetImages);
}

main();

3. JavaScript 模糊实现

javascript
function blurJS(data, width, height, radius) {
    const result = new Uint8ClampedArray(data.length);
    
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let rSum = 0, gSum = 0, bSum = 0, count = 0;
            
            for (let dy = -radius; dy <= radius; dy++) {
                for (let dx = -radius; dx <= radius; dx++) {
                    const ny = Math.min(height - 1, Math.max(0, y + dy));
                    const nx = Math.min(width - 1, Math.max(0, x + dx));
                    
                    const idx = (ny * width + nx) * 4;
                    rSum += data[idx];
                    gSum += data[idx + 1];
                    bSum += data[idx + 2];
                    count++;
                }
            }
            
            const idx = (y * width + x) * 4;
            result[idx] = rSum / count;
            result[idx + 1] = gSum / count;
            result[idx + 2] = bSum / count;
            result[idx + 3] = data[idx + 3];
        }
    }
    
    data.set(result);
}

function contrastJS(data, factor) {
    for (let i = 0; i < data.length; i += 4) {
        data[i] = Math.max(0, Math.min(255, (data[i] - 128) * factor + 128));
        data[i+1] = Math.max(0, Math.min(255, (data[i+1] - 128) * factor + 128));
        data[i+2] = Math.max(0, Math.min(255, (data[i+2] - 128) * factor + 128));
    }
}

性能对比结果

测试数据

滤镜WASM (ms)JavaScript (ms)加速比
灰度15453x
反色12383.2x
亮度18522.9x
对比度20552.75x
模糊 (1000x1000)1508505.7x

性能分析

┌──────────────────────────────────────────┐
│        性能对比分析                        │
├──────────────────────────────────────────┤
│                                          │
│  WASM 优势:                              │
│  - 预编译优化                             │
│  - 强类型,无动态开销                     │
│  - SIMD 指令支持                         │
│  - 紧凑内存布局                          │
│                                          │
│  JavaScript 优势:                        │
│  - JIT 编译优化                           │
│  - 灵活的类型系统                        │
│  - 丰富的 API                            │
│                                          │
│  性能差距大的场景:                        │
│  - 大量数学计算                          │
│  - 嵌套循环                              │
│  - 大数组处理                            │
│                                          │
└──────────────────────────────────────────┘

优化技巧

1. 避免频繁内存分配

rust
// ❌ 低效
#[wasm_bindgen]
pub fn process_each_pixel(data: &[u8]) -> Vec<u8> {
    data.iter().map(|b| b * 2).collect()  // 每次都分配新 Vec
}

// ✅ 高效:预分配
static mut BUFFER: Vec<u8> = Vec::new();

#[wasm_bindgen]
pub fn process_with_buffer(data: &[u8]) -> *mut u8 {
    unsafe {
        BUFFER.clear();
        BUFFER.reserve(data.len());
        BUFFER.extend(data.iter().map(|b| b * 2));
        BUFFER.as_mut_ptr()
    }
}
▶ Run

2. 使用 SIMD

rust
// 需要 nightly Rust
#![feature(portable_simd)]

use std::simd::*;

#[wasm_bindgen]
pub fn grayscale_simd(data: &mut [u8]) {
    for chunk in data.chunks_exact_mut(4) {
        let pixels = u8x4::from_slice(chunk);
        
        // SIMD 计算
        let gray = (pixels[0] as f32 * 0.299 + pixels[1] as f32 * 0.587 + pixels[2] as f32 * 0.114) as u8;
        
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
    }
}
▶ Run

3. 批量处理

rust
// ✅ 批量处理所有滤镜
#[wasm_bindgen]
pub fn apply_all_filters(data: &mut [u8], brightness: f32, contrast: f32) {
    // 一次性应用所有滤镜
    for chunk in data.chunks_exact_mut(4) {
        // 亮度
        chunk[0] = ((chunk[0] as f32 * brightness).min(255.0)) as u8;
        chunk[1] = ((chunk[1] as f32 * brightness).min(255.0)) as u8;
        chunk[2] = ((chunk[2] as f32 * brightness).min(255.0)) as u8;
        
        // 对比度
        chunk[0] = (((chunk[0] as f32 - 128.0) * contrast + 128.0).clamp(0.0, 255.0)) as u8;
        chunk[1] = (((chunk[1] as f32 - 128.0) * contrast + 128.0).clamp(0.0, 255.0)) as u8;
        chunk[2] = (((chunk[2] as f32 - 128.0) * contrast + 128.0).clamp(0.0, 255.0)) as u8;
    }
}
▶ Run

部署

1. 构建

bash
wasm-pack build --release --target web

2. 部署

bash
# 简单部署
python3 -m http.server 8080

# 或使用 npm
npm install -g serve
serve .

3. 打包

javascript
// 使用 webpack 或 rollup 打包
import init from './pkg/image_wasm.js';

// 预加载 WASM
const wasmPromise = init();

export async function processImage(filter, data) {
    await wasmPromise;
    return filter(data);
}

小结

实战项目核心:

  • 图像处理:灰度、反色、亮度、对比度、模糊
  • 性能对比:WASM vs JavaScript
  • Web 集成:Canvas、ImageData

关键技能:

  • WASM 函数导出
  • JavaScript 调用 WASM
  • 性能测试和对比
  • Web 前端集成

优化要点:

  • 避免频繁内存分配
  • 使用批量处理
  • 利用 SIMD(实验性)

恭喜完成 WebAssembly 学习! 你已经掌握了 WASM 基础、wasm-bindgen 和实战应用。

练习

练习 1:添加新滤镜

实现新的图像滤镜:

rust
// TODO: 实现边缘检测滤镜
// TODO: 实现锐化滤镜
// TODO: 对比性能
▶ Run

练习 2:批量处理

实现批量图像处理:

rust
// TODO: 处理多张图片
// TODO: 并发处理(使用 Web Workers)
// TODO: 显示进度
▶ Run

练习 3:优化性能

优化 WASM 性能:

rust
// TODO: 使用预分配缓冲区
// TODO: 减少 JavaScript ↔ WASM 调用
// TODO: 使用 SIMD(实验性)
▶ Run