agentleFS
adxptivedadxptived/Rust-Skills/skills/rust-wasm/SKILL.md

rust-wasm

Skill1 starsChanged 6 months agoMITView on GitHub

Rust WebAssembly development with wasm-bindgen, wasm-pack, web-sys, js-sys, and browser integration. Use when: - User compiles Rust to WebAssembly for web, workers, plugins, or edge runtimes - User uses wasm-bindgen, wasm-pack, web-sys, js-sys, gloo, trunk, or wasm-opt - User needs JS interop, typed bindings, memory transfer, or async browser APIs - User asks about WASM bundle size, panic hooks, console logging, or browser debugging - User integrates Rust WASM with React, Dioxus, Vite, npm packages, or web workers - User sees wasm32-unknown-unknown target errors or missing allocator/panic behavior - User needs shared memory, threads, atomics, or SIMD in WASM - User targets WASI or wasm32-wasip1 for server-side / edge runtimes

  • Installs packages

3 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

13 rules. 0 are common, 4 are distinctive, and 9 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 9

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep the Rust/JS boundary coarse-grained.

    line 26
  2. Distinctive:

    Treat browser APIs as fallible external systems.

    line 30
  3. Very specific:

    Use cfg(target_family = "wasm") for platform-specific code.

    line 31
  4. Very specific:

    Avoid wasm-bindgen on hot loops — pass whole buffers.

    line 32
  5. Very specific:

    Use &mut [u8] for image/audio/data buffers.

    line 93
  6. Very specific:

    Use serde_wasm_bindgen for complex return types, but avoid on hot paths where the serialization overhead matters.

    line 117
  7. Very specific:

    Prefer explicit bindings over passing untyped JsValue everywhere.

    line 195
  8. Very specific:

    Use &[u8] / Vec<u8> for zero-copy buffer sharing where possible.

    line 218
  9. Very specific:

    Run wasm-opt -Oz for size-sensitive web apps.

    line 328
  10. Distinctive:

    Keep exported functions stable and documented like a public API.

    line 329
  11. Very specific:

    Avoid serde_wasm_bindgen on hot paths unless ergonomics beat copy cost.

    line 330
  12. Very specific:

    Test core logic natively with normal Rust tests.

    line 331
  13. Distinctive:

    Verify target environment: browser main thread, worker, Node, edge runtime, or WASI.

    line 332

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust WebAssembly

Practical guide for shipping Rust code as WebAssembly: small boundaries, typed interop, predictable memory, and measurable bundle size.

Quick Navigation

  • references/bindings.md - wasm-bindgen, web-sys, JS types, async interop
  • references/bundling.md - wasm-pack, Trunk, Vite, size optimization, deployment

Golden Rules

  1. Keep the Rust/JS boundary coarse-grained.
  2. Pass bytes and IDs instead of many tiny objects.
  3. Enable panic hooks in debug builds, not production hot paths.
  4. Measure .wasm size after wasm-opt, not before.
  5. Treat browser APIs as fallible external systems.
  6. Use cfg(target_family = "wasm") for platform-specific code.
  7. Avoid wasm-bindgen on hot loops — pass whole buffers.

Project Setup

rustup target add wasm32-unknown-unknown
cargo install wasm-pack
cargo new image-core --lib
# Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"

[dependencies.web-sys]
version = "0.3"
features = ["console", "Window", "Document", "HtmlCanvasElement"]

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"

Export a Small API

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Filter {
    strength: f32,
}

#[wasm_bindgen]
impl Filter {
    #[wasm_bindgen(constructor)]
    pub fn new(strength: f32) -> Self {
        Self { strength }
    }

    pub fn apply_rgba(&self, pixels: &mut [u8]) {
        for chunk in pixels.chunks_exact_mut(4) {
            let gray = (chunk[0] as f32 * 0.299
                + chunk[1] as f32 * 0.587
                + chunk[2] as f32 * 0.114) as u8;
            for channel in &mut chunk[..3] {
                *channel = ((*channel as f32 * (1.0 - self.strength))
                    + (gray as f32 * self.strength)) as u8;
            }
        }
    }
}

Use &mut [u8] for image/audio/data buffers. Avoid one exported call per pixel or row.

Returning Complex Types

use serde::{Serialize, Deserialize};
use wasm_bindgen::prelude::*;

#[derive(Serialize, Deserialize)]
pub struct AnalysisResult {
    pub mean: f64,
    pub median: f64,
    pub std_dev: f64,
    pub count: usize,
}

#[wasm_bindgen]
pub fn analyze(data: &[f64]) -> JsValue {
    let mean = data.iter().sum::<f64>() / data.len() as f64;
    let result = AnalysisResult { mean, median: 0.0, std_dev: 0.0, count: data.len() };
    serde_wasm_bindgen::to_value(&result).unwrap()
}

Use serde_wasm_bindgen for complex return types, but avoid on hot paths where the serialization overhead matters.

Accepting JS Callbacks

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_with_progress(
    data: &[u8],
    on_progress: &js_sys::Function,
) -> Result<(), JsValue> {
    let total = data.len();
    for (i, chunk) in data.chunks(1024).enumerate() {
        // process chunk
        let pct = (i as f64 / total as f64) * 100.0;
        let this = JsValue::NULL;
        let args = js_sys::Array::of1(&JsValue::from_f64(pct));
        on_progress.call(&this, &args)?;
    }
    Ok(())
}

For hot paths, prefer passing a channel or buffer over function callbacks to avoid per-call overhead.

Wasm Threads (Shared Memory)

# Cargo.toml additional settings for threading
[package.metadata.wasm-pack.profile.release]
wasm-opt = false  # wasm-opt breaks shared memory
// Enable shared memory in the module
#[wasm_bindgen]
pub fn init_pool(num_workers: usize) -> Result<(), JsValue> {
    console_error_panic_hook::set_once();
    // Use wasm-bindgen-rayon for thread pool
    Ok(())
}

Threads require SharedArrayBuffer support, COOP/COEP headers, and wasm-opt compatibility flags.

Debug Hooks

#[cfg(debug_assertions)]
#[wasm_bindgen(start)]
pub fn init_debug() {
    console_error_panic_hook::set_once();
}
[target.'cfg(debug_assertions)'.dependencies]
console_error_panic_hook = "0.1"

JS Interop Shape

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(message: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    log(&format!("hello, {name}"));
}

Prefer explicit bindings over passing untyped JsValue everywhere.

Async Browser APIs

use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;

#[wasm_bindgen]
pub async fn fetch_text(url: String) -> Result<String, JsValue> {
    let window = web_sys::window().ok_or_else(|| JsValue::from_str("missing window"))?;
    let response = JsFuture::from(window.fetch_with_str(&url)).await?;
    let response: web_sys::Response = response.dyn_into()?;
    let text = JsFuture::from(response.text()?).await?;
    text.as_string().ok_or_else(|| JsValue::from_str("response text was not a string"))
}

Memory Management

Rust WASM memory (WASM linear memory) is separate from the JS heap:

  • wasm_bindgen copies strings and arrays by default.
  • Use &[u8] / Vec<u8> for zero-copy buffer sharing where possible.
  • Large structures are more efficient as serialized bytes than as many small JS objects.
  • Calling free() on JS objects (they're JsValue with Drop) is automatic when the Rust wrapper goes out of scope.

Size Optimization

wasm-pack build --release --target web
wasm-opt -Oz -o pkg/optimized.wasm pkg/my_crate_bg.wasm
# Check size
ls -lh pkg/optimized.wasm
TechniqueTypical Savings
opt-level = "z"15-25%
LTO + 1 codegen unit10-20%
wasm-opt -Oz20-40%
Remove unused featuresVariable
wee_alloc (legacy)Smaller binary, slower alloc

Modern Rust uses the default allocator; wee_alloc is rarely needed with LTO.

WASM in Web Workers

// worker.rs — compiled separately as a dedicated wasm module
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn worker_entry() -> Result<(), JsValue> {
    // Worker main loop — receives messages via browser postMessage
    Ok(())
}

Workers cannot access the DOM (web_sys::window() returns None). Use postMessage for communication.

WASI (Wasm on the Server)

rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release
# Run with wasmtime or wasmer
wasmtime run target/wasm32-wasip1/release/my-app.wasm

WASI gives you file I/O, clocks, and stdio — closer to a native binary than browser WASM.

Testing WASM

// tests/web.rs
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn test_filter_applies() {
    let filter = Filter::new(0.5);
    let mut pixels = vec![100u8, 150, 200, 255, 50, 75, 100, 255];
    filter.apply_rgba(&mut pixels);
    assert!(pixels[0] < 100); // darkened
}
wasm-pack test --firefox # or --chrome, --safari, --node
wasm-pack test --headless

Anti-Patterns

// Bad: thousands of boundary crossings.
#[wasm_bindgen]
pub fn set_pixel(x: u32, y: u32, r: u8, g: u8, b: u8) { /* ... */ }

// Good: one call over a whole buffer.
#[wasm_bindgen]
pub fn apply_image_filter(rgba: &mut [u8]) { /* ... */ }
// Bad: unchecked browser globals panic in workers or tests.
let document = web_sys::window().unwrap().document().unwrap();

// Good: model environment availability as Result.
let document = web_sys::window()
    .and_then(|window| window.document())
    .ok_or_else(|| JsValue::from_str("document unavailable"))?;
// Bad: re-serializing unchanged data on every frame.
let js_val = serde_wasm_bindgen::to_value(&my_data).unwrap();

// Good: cache the serialized form if data hasn't changed.
static CACHED: OnceLock<JsValue> = OnceLock::new();
// Bad: large data copied across boundary repeatedly.
// The input array is cloned into WASM memory every frame.

// Good: allocate once in WASM, pass the pointer, reuse the buffer.
// See wasm-bindgen's support for manual memory management.

Production Checklist

  • Build release artifacts with wasm-pack build --release or equivalent.
  • Run wasm-opt -Oz for size-sensitive web apps.
  • Keep exported functions stable and documented like a public API.
  • Avoid serde_wasm_bindgen on hot paths unless ergonomics beat copy cost.
  • Test core logic natively with normal Rust tests; test bindings with wasm-bindgen-test.
  • Verify target environment: browser main thread, worker, Node, edge runtime, or WASI.
  • Set panic = "abort" in release profile.
  • Measure bundle size in CI and set a budget.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.