Das Pattern
Ein Rust-Crate, kompiliert zu einem 20-KB-Wasm-Modul, ins Repo eingecheckt und ohne einen einzigen Vite-Plugin geladen. Der Dev-Container braucht nie eine Rust-Toolchain: ein Wegwerf-Builder-Container kompiliert das Crate, und Glue-Code samt Binary werden wie normaler Quellcode committet.
Der Kernel
Die Pixel liegen im linearen Wasm-Speicher. JS schreibt sie einmal durch eine View, Filteraufrufe überqueren die Grenze mit zwei Skalaren, und ein Vitest-Paritätsgate hält die Rust- und JS-Implementierungen byte-identisch.
#[wasm_bindgen]
pub struct PixelKernel {
width: u32,
height: u32,
pixels: Vec<u8>, // JS writes through a view over pixels_ptr() — once
scratch: Vec<u8>, // preallocated: no filter call may grow wasm memory
}
#[wasm_bindgen]
impl PixelKernel {
/// Filter calls cross the boundary with two scalars, never with the frame.
pub fn box_blur(&mut self, radius: u32) { /* (2r+1)² taps per pixel */ }
pub fn grayscale(&mut self) { /* Rec.601 integer luma */ }
}Der Build
Ein Host-Skript, ein ephemerer Rust-Container, committete Artefakte. Reproduzierbarkeit ist gepinnt über ein Digest-gepinntes Builder-Image, rust-toolchain.toml und Cargo.lock — und ein committetes Build-Manifest lässt das Gate rot werden, wenn Quellen und Artefakte auseinanderlaufen.
# scripts/wasm/build.sh — the v10r container stays Rust-free
RUST_IMAGE="docker.io/library/rust:1.97-slim@sha256:8e8cf8…" # digest-pinned
podman run --rm -v "$PWD:/work" -w /work/crates/kernel "$RUST_IMAGE" bash -c '
cargo build --release --target wasm32-unknown-unknown
wasm-bindgen --target web --out-dir /work/src/lib/wasm/kernel \
target/wasm32-unknown-unknown/release/v10r_kernel.wasm'
# kernel.js + kernel_bg.wasm (+ .d.ts) are COMMITTED, plus build-manifest.json:
# sha256 of sources and artifacts — a vitest gate recomputes them, so a Rust
# edit without a rebuild goes red. `bun run validate` never needs Rust.Der Loader
?url plus ein expliziter init-Aufruf umgeht jede bekannte Dev-vs-Build-Divergenz von Wasm in Vite und SvelteKit — kein Wasm-Plugin, kein Top-Level-Await, nichts läuft während SSR.
import wasmUrl from './kernel/kernel_bg.wasm?url';
let ready: Promise<Kernel> | null = null;
export function loadKernel(): Promise<Kernel> {
// ?url + explicit init: no Vite wasm plugins, no top-level await (broken
// under Svelte 5 — sveltejs/kit#13015), nothing executed during SSR, and
// the binary ships as a hashed immutable asset.
if (!ready) {
ready = import('./kernel/kernel.js').then(async (mod) => {
const out = await mod.default({ module_or_path: wasmUrl });
return { mod, memory: out.memory };
});
}
return ready;
}