Integrar WebAssembly con Claude Code: Rust, wasm-pack y Vite
Integra WebAssembly en Vite con Claude Code, Rust, wasm-pack, wrappers tipados, benchmarks y trampas comunes.
Qué debe hacer Claude Code con WebAssembly
WebAssembly, o Wasm, es un formato binario portable que permite ejecutar código escrito en Rust, C, C++ y otros lenguajes dentro del navegador o Node.js. No conviene verlo como un reemplazo de JavaScript. En aplicaciones reales funciona mejor como acelerador de una parte concreta: procesamiento de imágenes, compresión, manejo de bytes para criptografía, cálculo numérico, agregación de CSV o reutilización de una librería Rust/C++ ya existente.
Claude Code aporta valor porque esta integración no vive en un solo archivo. Hace falta una función Rust, una compilación con wasm-pack, el pegamento generado por wasm-bindgen, una carga asíncrona desde Vite, un wrapper TypeScript, un benchmark y una revisión que mire costes de frontera. La frontera JS-Wasm es el punto donde los datos pasan de JavaScript a WebAssembly. Si esa frontera se cruza miles de veces con llamadas pequeñas, la supuesta mejora puede desaparecer.
En este artículo construiremos una base pequeña y copiable: invertir un buffer RGBA, sumar una columna numérica de un CSV y calcular un checksum ligero sobre bytes. Estos tres ejemplos cubren imagen, texto y datos binarios. Desde ahí puedes avanzar hacia procesamiento rápido dentro del navegador, portabilidad de código Rust/C++, códecs propios, compresión o cálculos locales cuando no quieres enviar datos al servidor. Para el contexto general de rendimiento, revisa también Claude Code performance optimization.
Trabaja con la documentación oficial abierta: MDN WebAssembly para la plataforma, wasm-bindgen Guide para el puente Rust-JavaScript y wasm-pack repository para el flujo de build. Pídele a Claude Code que respete esos límites antes de inventar un cargador personalizado.
Elegir el caso de uso antes de programar
Wasm no es más rápido por definición. Es bueno cuando le pasas un bloque grande de trabajo y lo resuelve con bucles ajustados. Es malo cuando lo llamas desde un bucle JavaScript para procesar unidades diminutas. Por eso el primer prompt no debería ser “hazlo con Wasm”, sino “identifica qué operación merece cruzar la frontera y cómo la mediremos”.
| Caso de uso | Por qué encaja con Wasm | Qué debe revisar Claude Code |
|---|---|---|
| Procesamiento de imágenes | Los buffers RGBA se recorren con bucles simples | Copias de memoria, lectura de Canvas y benchmark justo |
| Criptografía, compresión, códecs | Trabajan con bytes y pueden reutilizar librerías Rust | Si hace falta una librería auditada y qué no debe implementarse a mano |
| CSV y cálculo numérico | Parsing y agregación repiten muchas operaciones | Filas vacías, NaN, archivos grandes y estrategia de errores |
| Migrar Rust o C++ existente | Permite reutilizar lógica probada en el navegador | APIs del sistema, I/O, threads y dependencias no compatibles |
| Procesamiento local en navegador | Mantiene datos sensibles en el dispositivo | Tamaño inicial, fallback y navegadores objetivo |
En las pruebas de Masa, el camino más claro fue convertir una sola función y medir. En imágenes, la función Rust era rápida, pero el tiempo de leer y escribir ImageData podía dominar. En CSV, pasar el texto completo una vez fue mucho mejor que llamar a Wasm por cada línea. Esas restricciones deben estar en el prompt de Claude Code desde el principio.
Módulo Rust mínimo con wasm-pack
wasm-pack compila el crate Rust, ejecuta wasm-bindgen y genera una carpeta pkg con el binario Wasm, el cargador JavaScript, metadatos y declaraciones TypeScript. wasm-bindgen es la librería que expone funciones Rust seleccionadas al mundo JavaScript.
# Cargo.toml
[package]
name = "wasm-lab"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn invert_rgba(pixels: &mut [u8]) {
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
#[wasm_bindgen]
pub fn sum_csv_column(csv: &str, column: usize) -> f64 {
csv.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| line.split(',').nth(column))
.filter_map(|cell| cell.trim().parse::<f64>().ok())
.sum()
}
#[wasm_bindgen]
pub fn fnv1a32(bytes: &[u8]) -> u32 {
let mut hash = 0x811c9dc5u32;
for byte in bytes {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(0x01000193);
}
hash
}
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build --target web --out-dir pkg
La función fnv1a32 no es un hash criptográfico seguro. Para contraseñas, firmas, pagos o tokens, usa Web Crypto API o una librería auditada. Aquí solo sirve para mostrar el paso de un array de bytes por la frontera JS-Wasm.
Cargar desde Vite con un wrapper tipado
Después del build tendrás pkg/wasm_lab.js y pkg/wasm_lab.d.ts. En Vite, importa el módulo generado, espera init() y esconde esa inicialización detrás de un wrapper. Así la UI no llama funciones Wasm antes de tiempo ni repite la inicialización en cada acción.
// src/wasm-client.ts
import init, {
fnv1a32,
invert_rgba,
sum_csv_column,
} from "../pkg/wasm_lab";
export type WasmClient = {
invertImage(imageData: ImageData): Promise<ImageData>;
sumCsvColumn(csv: string, columnIndex: number): Promise<number>;
checksum(bytes: Uint8Array): Promise<number>;
};
let initPromise: Promise<void> | undefined;
async function ensureWasm(): Promise<void> {
initPromise ??= init().then(() => undefined);
return initPromise;
}
export const wasmClient: WasmClient = {
async invertImage(imageData) {
await ensureWasm();
const pixels = new Uint8Array(
imageData.data.buffer,
imageData.data.byteOffset,
imageData.data.byteLength,
);
invert_rgba(pixels);
return imageData;
},
async sumCsvColumn(csv, columnIndex) {
await ensureWasm();
return sum_csv_column(csv, columnIndex);
},
async checksum(bytes) {
await ensureWasm();
return fnv1a32(bytes);
},
};
// src/main.ts
import { wasmClient } from "./wasm-client";
const fileInput = document.querySelector<HTMLInputElement>("#csv-file");
const output = document.querySelector<HTMLPreElement>("#output");
fileInput?.addEventListener("change", async () => {
const file = fileInput.files?.[0];
if (!file || !output) return;
const csv = await file.text();
const total = await wasmClient.sumCsvColumn(csv, 2);
output.textContent = `column 2 total: ${total.toFixed(2)}`;
});
Para este flujo con wasm-pack --target web, empieza con Vite estándar. Añade plugins solo si vas a importar .wasm crudo o si tu bundling exige otra estrategia. La mayoría de fallos iniciales están en rutas e inicialización, no en la falta de un plugin.
Prompt de revisión para Claude Code
Claude Code debe implementar y luego revisar de forma crítica. El prompt de revisión tiene que ser concreto: inicialización asíncrona, copias de memoria, frontera JS-Wasm, DOM, tipos y comandos de verificación.
Review only these files:
- src/lib.rs
- pkg/wasm_lab.d.ts
- src/wasm-client.ts
- src/main.ts
- src/bench.ts
Goal:
Integrate the Rust WebAssembly module into the Vite app without changing UI behavior.
Check:
1. init() is awaited before any exported Wasm function is called.
2. init() is cached and not repeated for every click or file upload.
3. Large arrays cross the JS-Wasm boundary at most once per user action.
4. DOM updates stay in TypeScript, not inside Rust.
5. The wrapper exposes typed methods and keeps generated pkg files out of hand edits.
6. Benchmarks compare the same input data for JavaScript and Wasm.
Run:
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build
Si tu equipo usa Claude Code a diario, copia estas reglas en CLAUDE.md. Así cada cambio de Wasm recibe la misma revisión, incluso cuando cambia la persona que hace el PR.
Benchmark y verificación
No apruebes una migración a Wasm por sensación. Mide el mismo input y el mismo resultado. Este benchmark compara inversión RGBA en JavaScript y Wasm.
// src/bench.ts
import { wasmClient } from "./wasm-client";
function invertJs(pixels: Uint8Array): void {
for (let index = 0; index < pixels.length; index += 4) {
pixels[index] = 255 - pixels[index];
pixels[index + 1] = 255 - pixels[index + 1];
pixels[index + 2] = 255 - pixels[index + 2];
}
}
function cloneImageData(source: Uint8Array, width: number, height: number): ImageData {
return new ImageData(new Uint8ClampedArray(source), width, height);
}
export async function runBench(): Promise<void> {
const width = 1920;
const height = 1080;
const source = new Uint8Array(width * height * 4);
crypto.getRandomValues(source);
const jsPixels = new Uint8Array(source);
const wasmImage = cloneImageData(source, width, height);
const jsStart = performance.now();
invertJs(jsPixels);
const jsMs = performance.now() - jsStart;
const wasmStart = performance.now();
await wasmClient.invertImage(wasmImage);
const wasmMs = performance.now() - wasmStart;
console.table({
javascriptMs: Number(jsMs.toFixed(2)),
wasmMs: Number(wasmMs.toFixed(2)),
ratio: Number((jsMs / wasmMs).toFixed(2)),
});
}
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build
npm run dev
Si Wasm no gana, revisa conversiones antes de culpar a Rust. ImageData, Canvas, strings y builds de desarrollo pueden ocultar el coste real. Pega el resultado en Claude Code y pide una recomendación: mantener Wasm, mover el trabajo a un Web Worker o optimizar JavaScript.
Trampas habituales
La primera trampa es la inicialización asíncrona. init() debe resolverse antes de llamar cualquier export. Cachea esa promesa en un wrapper.
La segunda es el tamaño del bundle. Cada crate de Rust puede aumentar el .wasm. Empieza pequeño y revisa el build de producción.
La tercera es el coste de frontera JS-Wasm. Evita llamar Wasm desde bucles pequeños; pasa arrays, strings o buffers grandes.
La cuarta es intentar tocar el DOM desde Wasm. Eventos, render, accesibilidad y mensajes de error deben quedar en TypeScript.
La quinta es ignorar copias de memoria. Typed arrays, strings e ImageData pueden copiarse en los bindings. El benchmark debe incluir esa conversión.
La sexta es compatibilidad del navegador y headers. Wasm básico está muy soportado, pero Wasm threads y SharedArrayBuffer requieren COOP y COEP. Con anuncios, iframes o CDN, pruébalo pronto.
Operación de equipo y CTA
Para una prueba individual, este código basta. Para un equipo, define qué lógica va a Rust, qué sigue en TypeScript, cómo se tratan los archivos generados en pkg, qué navegadores se soportan y qué benchmark bloquea el merge. Esa política debe vivir en CLAUDE.md y en los prompts de revisión.
ClaudeCodeLab puede ayudar a convertir esto en un flujo de trabajo real: elegir el caso de uso correcto, revisar activos Rust/C++, diseñar benchmarks y formar al equipo para usar Claude Code sin saltarse seguridad ni rendimiento. Si WebAssembly afecta producción, privacidad o arquitectura frontend compartida, empieza por Claude Code training and consultation.
Nota de verificación
Al probar este flujo, el problema inicial no fue Rust sino el momento de llamar init(). Al mover la inicialización a wasm-client.ts, imagen, CSV y checksum siguieron el mismo camino. Con inputs pequeños JavaScript fue suficiente; con buffers Full HD y CSV grandes la diferencia se vio mejor. La lección práctica es medir la frontera completa, no solo el cuerpo de la función.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.