Claude Code for WebAssembly: Rust, C++, and Performance-Critical Code — Claude Skills 360 Blog
Blog / Development / Claude Code for WebAssembly: Rust, C++, and Performance-Critical Code
Development

Claude Code for WebAssembly: Rust, C++, and Performance-Critical Code

Published: June 23, 2026
Read time: 9 min read
By: Claude Skills 360

WebAssembly is the right tool for a specific set of problems: computationally intensive code that JavaScript can’t run fast enough, porting existing C/C++ libraries to the browser, and near-native performance for image/video processing, cryptography, and physics simulations. Claude Code helps navigate the WASM toolchain — which is non-trivial — and generates idiomatic Rust with wasm-bindgen that interops cleanly with JavaScript.

This guide covers WebAssembly with Claude Code: Rust modules, C++ via Emscripten, JavaScript interop, and performance optimization.

When to Use WebAssembly

Before reaching for WASM, Claude Code will help evaluate whether it’s actually needed:

We're doing real-time image filtering in the browser.
Users are reporting lag on the canvas operations.
Should we use WebAssembly?

Claude Code’s typical response: “For pixel-by-pixel operations on large canvases, yes — WASM gives you 5-10x faster loops because you avoid JavaScript’s typed array bounds checks and JIT deoptimizations. The setup overhead is worthwhile. For DOM manipulation or JSON parsing, stick with JavaScript.”

Good fits for WASM:

  • Image/video processing (pixel manipulation)
  • Audio processing (DSP, codecs)
  • Physics engines and collision detection
  • Cryptography (key derivation, hashing)
  • Compression/decompression
  • Porting mature C/C++ libraries (SQLite, ffmpeg, etc.)

Not worth WASM:

  • Most web app logic (routing, state, DOM manipulation)
  • Simple math that JavaScript optimizes well
  • Network operations (still gated by the network)

Rust + wasm-bindgen

CLAUDE.md for WASM Projects

## Rust WebAssembly Module

- Toolchain: wasm-pack (wraps wasm-bindgen and optimizes output)
- Target: wasm32-unknown-unknown
- JS integration: wasm-bindgen + web-sys + js-sys
- Bundler: Vite with vite-plugin-wasm

## Build commands
- Dev: `wasm-pack build --target web --dev`
- Release: `wasm-pack build --target web --release` (runs wasm-opt)
- Test: `wasm-pack test --headless --chrome`

## Important
- Keep WASM module focused — avoid pulling in heavy dependencies
- Use `#[wasm_bindgen]` only on public API boundaries
- Internal Rust code stays idiomatic Rust (no WASM-specific code inside)
- Memory: Rust owns WASM memory — pass data by value across the boundary
- Large data: use shared memory views (Uint8Array.from WASM memory) to avoid copying

Image Processing Module

// src/lib.rs
use wasm_bindgen::prelude::*;

// Grayscale conversion — pixel-by-pixel, 4x faster than equivalent JS
#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
    // data is RGBA — 4 bytes per pixel
    for pixel in data.chunks_exact_mut(4) {
        let r = pixel[0] as u32;
        let g = pixel[1] as u32;
        let b = pixel[2] as u32;
        
        // Luminosity formula (perceptually weighted)
        let gray = ((r * 299 + g * 587 + b * 114) / 1000) as u8;
        
        pixel[0] = gray;
        pixel[1] = gray;
        pixel[2] = gray;
        // pixel[3] is alpha — leave untouched
    }
}

// Gaussian blur — too slow in JS at 4K resolution
#[wasm_bindgen]
pub fn blur(data: &[u8], width: u32, height: u32, radius: u32) -> Vec<u8> {
    let mut output = data.to_vec();
    
    // Horizontal pass
    horizontal_blur(data, &mut output, width, height, radius);
    
    // Vertical pass (operate on result of horizontal pass)
    let temp = output.clone();
    vertical_blur(&temp, &mut output, width, height, radius);
    
    output
}

// Box blur — horizontal pass
fn horizontal_blur(src: &[u8], dst: &mut [u8], width: u32, height: u32, radius: u32) {
    let kernel_size = (2 * radius + 1) as usize;
    
    for y in 0..height as usize {
        for x in 0..width as usize {
            let mut r_sum = 0u32;
            let mut g_sum = 0u32;
            let mut b_sum = 0u32;
            let mut count = 0u32;
            
            for kx in x.saturating_sub(radius as usize)..=(x + radius as usize).min(width as usize - 1) {
                let idx = (y * width as usize + kx) * 4;
                r_sum += src[idx] as u32;
                g_sum += src[idx + 1] as u32;
                b_sum += src[idx + 2] as u32;
                count += 1;
            }
            
            let idx = (y * width as usize + x) * 4;
            dst[idx] = (r_sum / count) as u8;
            dst[idx + 1] = (g_sum / count) as u8;
            dst[idx + 2] = (b_sum / count) as u8;
            dst[idx + 3] = src[idx + 3]; // Preserve alpha
        }
    }
}

JavaScript Integration

// src/image-processor.ts
import init, { grayscale, blur } from '../wasm/pkg/image_processor';

let wasmInitialized = false;

async function ensureWasm() {
  if (!wasmInitialized) {
    await init();
    wasmInitialized = true;
  }
}

export async function applyGrayscale(canvas: HTMLCanvasElement): Promise<void> {
  await ensureWasm();
  
  const ctx = canvas.getContext('2d')!;
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  
  // imageData.data is a Uint8ClampedArray backed by WASM memory
  grayscale(imageData.data);
  
  ctx.putImageData(imageData, 0, 0);
}

export async function applyBlur(
  canvas: HTMLCanvasElement,
  radius: number,
): Promise<void> {
  await ensureWasm();
  
  const ctx = canvas.getContext('2d')!;
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  
  // blur() returns a new Vec<u8> — copy into imageData
  const blurred = blur(imageData.data, canvas.width, canvas.height, radius);
  const output = ctx.createImageData(canvas.width, canvas.height);
  output.data.set(blurred);
  
  ctx.putImageData(output, 0, 0);
}
// Vite config — enable WASM
// vite.config.ts
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';

export default defineConfig({
  plugins: [wasm(), topLevelAwait()],
});

C++ via Emscripten

We have a C++ library for MP3 encoding.
Make it callable from JavaScript in the browser.
// src/mp3encoder.cpp
#include <emscripten/bind.h>
#include <lame/lame.h>
#include <vector>

class Mp3Encoder {
private:
    lame_global_flags* gfp;
    
public:
    Mp3Encoder(int sampleRate, int channels, int bitrate) {
        gfp = lame_init();
        lame_set_in_samplerate(gfp, sampleRate);
        lame_set_num_channels(gfp, channels);
        lame_set_brate(gfp, bitrate);
        lame_set_quality(gfp, 2); // 2 = high quality
        lame_init_params(gfp);
    }
    
    ~Mp3Encoder() {
        lame_close(gfp);
    }
    
    std::vector<uint8_t> encode(
        const std::vector<float>& leftChannel,
        const std::vector<float>& rightChannel
    ) {
        int numSamples = leftChannel.size();
        std::vector<uint8_t> mp3Buffer(numSamples * 5 / 4 + 7200);
        
        int bytesEncoded = lame_encode_buffer_ieee_float(
            gfp,
            leftChannel.data(),
            rightChannel.data(),
            numSamples,
            mp3Buffer.data(),
            mp3Buffer.size()
        );
        
        if (bytesEncoded < 0) throw std::runtime_error("Encoding failed");
        
        mp3Buffer.resize(bytesEncoded);
        return mp3Buffer;
    }
    
    std::vector<uint8_t> flush() {
        std::vector<uint8_t> buffer(7200);
        int bytes = lame_encode_flush(gfp, buffer.data(), buffer.size());
        buffer.resize(bytes);
        return buffer;
    }
};

EMSCRIPTEN_BINDINGS(mp3encoder) {
    emscripten::register_vector<float>("VectorFloat");
    emscripten::register_vector<uint8_t>("VectorUint8");
    
    emscripten::class_<Mp3Encoder>("Mp3Encoder")
        .constructor<int, int, int>()
        .function("encode", &Mp3Encoder::encode)
        .function("flush", &Mp3Encoder::flush);
}
# Compile with Emscripten
emcc src/mp3encoder.cpp \
  -O3 \
  -lembind \
  -lmp3lame \
  -o dist/mp3encoder.js \
  -s MODULARIZE=1 \
  -s EXPORT_NAME=createMp3EncoderModule \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s ENVIRONMENT='web'

Binary Size Optimization

Our WASM module is 2MB. Users on mobile are complaining about load time.
Reduce the binary size.

Claude Code identifies and applies these optimizations:

# Cargo.toml
[profile.release]
opt-level = 'z'      # Optimize for binary size (not speed)
lto = true           # Link-time optimization — removes dead code
codegen-units = 1    # More aggressive dead code elimination
panic = 'abort'      # Removes the unwinding machinery (~100KB)
strip = true         # Strip debug symbols
# wasm-pack applies wasm-opt automatically in release mode
# But you can run it manually for more control:
wasm-opt -Oz --strip-debug -o optimized.wasm input.wasm

# Result: typically 50-70% size reduction from release build defaults

For Rust in non-WASM contexts like CLI tools and system services, see the Rust guide. For performance optimization of JavaScript code before considering WASM, the performance guide covers profiling and optimization patterns that often eliminate the need for WASM. The Claude Skills 360 bundle includes WebAssembly skill sets for Rust-to-WASM workflows. Start with the free tier to try WASM module generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free