Zum Inhalt springen

WebAssembly für Webentwickler verstehen

Praxisnahe Einführung in WebAssembly für Webentwickler: was es ist, wann es sich gegenüber JavaScript lohnt, Kompilieren aus Rust und C, Integration.

4 Min. Lesezeit
Diagramm zum Laden eines WebAssembly-Moduls, das die Kompilierung vom Rust-Quellcode über die Wasm-Binärdatei bis zur JavaScript-Integration zeigt

WebAssembly (Wasm) ist ein binäres Instruktionsformat, das in Webbrowsern mit nahezu nativer Geschwindigkeit ausgeführt wird. Es ersetzt JavaScript nicht, sondern ergänzt es für bestimmte Workloads, bei denen JavaScript zu langsam ist. Bildverarbeitung, Videokodierung, physikalische Simulationen, Kryptografie und computergestützte Geometrie profitieren allesamt von der berechenbaren Performance von Wasm.

Das gedankliche Modell dahinter: performancekritischen Code in Rust, C oder C++ schreiben, zu Wasm kompilieren und aus JavaScript heraus aufrufen. Die gesamte Anwendung bleibt in JavaScript — nur der Hot Path läuft in Wasm.

Wann Wasm sinnvoll ist (und wann nicht)

Wasm ist nicht in jedem Fall schneller als JavaScript. Bei DOM-Manipulation, Event-Handling und der typischen Logik einer Webanwendung ist JavaScript bereits gut optimiert. Wasm spielt seine Stärken bei CPU-intensiven Berechnungen aus, die der JIT-Compiler von JavaScript nur schwer optimieren kann.

tstypescript
// ❌ Using Wasm for DOM manipulation — no benefit, added complexity
// Wasm can't access the DOM directly — it calls JavaScript anyway
// The overhead of crossing the Wasm/JS boundary negates any speed gain
 
// ✅ Using Wasm for CPU-bound computation
const useCases = [
  'Image/video processing (filters, encoding, decoding)',
  'Cryptographic operations (hashing, encryption)',
  'Physics engines (game simulations, collision detection)',
  'Data compression (gzip, brotli, zstd)',
  'Scientific computing (matrix operations, signal processing)',
  'PDF generation and manipulation',
  'Audio synthesis and DSP',
] as const;
 
// Rule of thumb: if the operation takes >10ms in JavaScript
// AND does not need frequent DOM access, Wasm may help

Hello World: Von Rust zu WebAssembly

Rust bietet über wasm-pack erstklassige Wasm-Unterstützung. Hier ein minimales Beispiel — eine Funktion, die Fibonacci-Zahlen effizient berechnet.

shbash
# Install Rust and wasm-pack
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install wasm-pack
 
# Create a new Wasm library project
cargo new --lib fibonacci-wasm
cd fibonacci-wasm
tomltoml
# Cargo.toml
[package]
name = "fibonacci-wasm"
version = "0.1.0"
edition = "2021"
 
[lib]
crate-type = ["cdylib"]
 
[dependencies]
wasm-bindgen = "0.2"
rsrust
// src/lib.rs
use wasm_bindgen::prelude::*;
 
#[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
}
 
#[wasm_bindgen]
pub fn fibonacci_batch(numbers: &[u32]) -> Vec<u64> {
    numbers.iter().map(|&n| fibonacci(n)).collect()
}
shbash
# Build the Wasm package
wasm-pack build --target web --release
 
# This generates:
# pkg/fibonacci_wasm_bg.wasm  — the binary module
# pkg/fibonacci_wasm.js       — JavaScript glue code
# pkg/fibonacci_wasm.d.ts     — TypeScript type definitions

Wasm in einer Webanwendung laden

Der generierte JavaScript-Glue-Code übernimmt die Instanziierung. Importiere ihn wie jedes andere ES-Modul.

htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Wasm Fibonacci</title>
</head>
<body>
  <script type="module">
    import init, { fibonacci, fibonacci_batch } from './pkg/fibonacci_wasm.js';
 
    async function main() {
      // Initialize the Wasm module (loads and compiles the .wasm file)
      await init();
 
      // Call Wasm functions like regular JavaScript functions
      console.log(fibonacci(10));  // 55
      console.log(fibonacci(50));  // 12586269025
 
      // Batch processing
      const inputs = new Uint32Array([10, 20, 30, 40, 50]);
      const results = fibonacci_batch(inputs);
      console.log(results);  // [55, 6765, 832040, 102334155, 12586269025]
    }
 
    main();
  </script>
</body>
</html>
tstypescript
// In a bundled application (Vite, Webpack, etc.)
import init, { fibonacci } from 'fibonacci-wasm';
 
async function setupWasm() {
  await init(); // Must be called once before using any Wasm function
 
  // Now use Wasm functions anywhere
  const result = fibonacci(45);
  console.log(result);
}
 
setupWasm();

Praxisbeispiel: Bildverarbeitung

Hier ein praktisches Beispiel — ein Wasm-Modul, das einen Graustufenfilter auf die Pixeldaten eines Bildes anwendet.

rsrust
// src/lib.rs — image processing functions
use wasm_bindgen::prelude::*;
 
#[wasm_bindgen]
pub fn grayscale(pixels: &mut [u8]) {
    // Pixels are in RGBA format — 4 bytes per pixel
    for chunk in pixels.chunks_exact_mut(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        // Luminance formula (ITU-R BT.709)
        let gray = (0.2126 * r + 0.7152 * g + 0.0722 * b) as u8;
        chunk[0] = gray;
        chunk[1] = gray;
        chunk[2] = gray;
        // chunk[3] (alpha) stays unchanged
    }
}
 
#[wasm_bindgen]
pub fn brightness(pixels: &mut [u8], adjustment: i32) {
    for chunk in pixels.chunks_exact_mut(4) {
        for i in 0..3 {
            let value = chunk[i] as i32 + adjustment;
            chunk[i] = value.clamp(0, 255) as u8;
        }
    }
}
 
#[wasm_bindgen]
pub fn contrast(pixels: &mut [u8], factor: f32) {
    for chunk in pixels.chunks_exact_mut(4) {
        for i in 0..3 {
            let value = ((chunk[i] as f32 - 128.0) * factor + 128.0) as i32;
            chunk[i] = value.clamp(0, 255) as u8;
        }
    }
}
tstypescript
// JavaScript side — using Wasm for canvas image processing
import init, { grayscale, brightness } from './pkg/image_processor.js';
 
async function applyGrayscaleFilter(canvas: HTMLCanvasElement) {
  await init();
 
  const ctx = canvas.getContext('2d')!;
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
 
  // Pass pixel array directly to Wasm — zero-copy via shared memory
  grayscale(imageData.data);
 
  // Write modified pixels back to canvas
  ctx.putImageData(imageData, 0, 0);
}
 
// ❌ Processing pixels in JavaScript
function grayscaleJS(imageData: ImageData) {
  const pixels = imageData.data;
  for (let i = 0; i < pixels.length; i += 4) {
    const gray = 0.2126 * pixels[i] + 0.7152 * pixels[i+1] + 0.0722 * pixels[i+2];
    pixels[i] = pixels[i+1] = pixels[i+2] = gray;
  }
  // 4K image: ~45ms in JavaScript
}
 
// ✅ Processing pixels in Wasm
// Same 4K image: ~8ms in Wasm — 5.6x faster
// The difference grows with image size and filter complexity

Strategie für den Performance-Vergleich

Führe immer Benchmarks durch, bevor du dich für Wasm entscheidest. Die Grenze zwischen Wasm und JS bringt Overhead mit sich — kleine, häufige Aufrufe können den Geschwindigkeitsvorteil zunichtemachen.

tstypescript
// Benchmarking Wasm vs JavaScript
async function benchmark() {
  await init();
 
  const iterations = 1000;
 
  // Benchmark JavaScript
  const jsStart = performance.now();
  for (let i = 0; i < iterations; i++) {
    fibonacciJS(40); // JavaScript implementation
  }
  const jsTime = performance.now() - jsStart;
 
  // Benchmark Wasm
  const wasmStart = performance.now();
  for (let i = 0; i < iterations; i++) {
    fibonacci(40); // Wasm implementation
  }
  const wasmTime = performance.now() - wasmStart;
 
  console.log(`JavaScript: ${jsTime.toFixed(2)}ms`);
  console.log(`Wasm:       ${wasmTime.toFixed(2)}ms`);
  console.log(`Speedup:    ${(jsTime / wasmTime).toFixed(2)}x`);
}
 
// Typical results for computational workloads:
// fibonacci(40) x1000: JS ~320ms, Wasm ~85ms → 3.8x faster
// Image grayscale 4K:  JS ~45ms,  Wasm ~8ms  → 5.6x faster
// JSON parsing:        JS ~2ms,   Wasm ~3ms  → JS is faster (V8 optimizes this)
 
// ❌ Moving JSON parsing to Wasm — JS already optimized for this
// ✅ Moving matrix multiplication to Wasm — consistent 4-6x speedup

Die wichtigsten Erkenntnisse

  1. Wasm ergänzt JavaScript, es ersetzt es nicht — setze es für CPU-intensive Berechnungen ein, bei denen JavaScript nachweislich zu langsam ist
  2. Rust ist die beste Ausgangssprache für Wasm — wasm-pack erzeugt mit minimalem Aufwand typsichere JavaScript-Bindings
  3. Fasse Operationen über die Wasm-Grenze hinweg zusammen — ein einzelner Aufruf mit einem großen Array ist schneller als viele Aufrufe mit einzelnen Werten
  4. Benchmarke immer, bevor du dich festlegst — manche Workloads (JSON-Parsing, DOM-Manipulation) sind in JavaScript bereits optimiert
  5. Der Aufruf von init() lädt und kompiliert das Modul — rufe ihn einmal beim Start auf, nicht vor jedem einzelnen Funktionsaufruf
  6. Bild- und Audioverarbeitung sind ideale Anwendungsfälle für Wasm — vorhersagbare Datenlayouts und intensive Berechnungen führen direkt zu Geschwindigkeitsgewinnen
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX