Saltar al contenido

Entendiendo WebAssembly para desarrolladores web

Introducción práctica a WebAssembly para desarrolladores web: qué es, cuándo usarlo en lugar de JavaScript, cómo compilar desde Rust y C, e integrarlo.

5 min de lectura
Diagrama de carga de un módulo WebAssembly que muestra la compilación desde el código fuente en Rust, pasando por el binario Wasm, hasta la integración con JavaScript

WebAssembly (Wasm) es un formato de instrucciones binario que se ejecuta en los navegadores web a una velocidad cercana a la nativa. No reemplaza a JavaScript, sino que lo complementa para cargas de trabajo específicas en las que JavaScript resulta demasiado lento. El procesamiento de imágenes, la codificación de video, las simulaciones físicas, la criptografía y la geometría computacional se benefician del rendimiento predecible de Wasm.

El modelo mental es sencillo: escribe el código crítico para el rendimiento en Rust, C o C++, compílalo a Wasm y llámalo desde JavaScript. Toda tu aplicación permanece en JavaScript; solo la ruta crítica se ejecuta en Wasm.

Cuándo tiene sentido usar Wasm (y cuándo no)

Wasm no es más rápido que JavaScript en todos los casos. Para la manipulación del DOM, el manejo de eventos y la lógica típica de una aplicación web, JavaScript ya está optimizado. Wasm brilla en cálculos intensivos de CPU que al compilador JIT de JavaScript le cuesta optimizar.

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

Hola mundo: de Rust a WebAssembly

Rust ofrece soporte de primer nivel para Wasm mediante wasm-pack. A continuación, un ejemplo mínimo: una función que calcula números de Fibonacci de forma eficiente.

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

Cómo cargar Wasm en una aplicación web

El código de enlace (glue code) en JavaScript generado se encarga de la instanciación. Impórtalo como cualquier otro módulo ES.

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();

Ejemplo real: procesamiento de imágenes

Este es un ejemplo práctico: un módulo Wasm que aplica un filtro de escala de grises a los datos de píxeles de una imagen.

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

Estrategia de comparación de rendimiento

Realiza siempre pruebas de rendimiento antes de decidirte por Wasm. El límite entre Wasm y JS tiene un costo asociado: las llamadas pequeñas y frecuentes pueden anular la ventaja de velocidad.

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

Puntos clave

  1. Wasm complementa a JavaScript, no lo reemplaza — úsalo para cálculos intensivos de CPU donde JavaScript sea, de forma medible, demasiado lento
  2. Rust es el mejor lenguaje de origen para Wasm — wasm-pack genera bindings de JavaScript con tipado seguro con un esfuerzo mínimo
  3. Agrupa las operaciones al cruzar el límite de Wasm — una sola llamada con un array grande es más rápida que muchas llamadas con valores individuales
  4. Haz siempre pruebas de rendimiento antes de comprometerte — algunas cargas de trabajo (parseo de JSON, manipulación del DOM) ya están optimizadas en JavaScript
  5. La llamada a init() carga y compila el módulo — llámala una sola vez al iniciar, no antes de cada llamada a una función
  6. El procesamiento de imágenes y audio son casos de uso ideales para Wasm — los diseños de datos predecibles y el cómputo pesado se traducen directamente en mejoras de velocidad
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX