Las seis lecciones anteriores tratan de JavaScript hablando con el navegador: guardar, pedir, sincronizar, cachear, formatear. Esta trata de algo distinto: código que no es JavaScript ejecutándose en la misma página, dentro del mismo motor, a velocidad casi nativa. WebAssembly —Wasm para los amigos— es un formato binario portable que permite traer a la web programas escritos en C, C++, Rust o Go. Gracias a él existen editores de vídeo, motores de juego, herramientas de CAD y bases de datos completas funcionando dentro de una pestaña. En esta lección entenderás qué es y, sobre todo, qué no es; verás su modelo de ejecución y por qué solo entiende números; cargarás e instanciarás un módulo desde JavaScript; conocerás las herramientas reales de cada lenguaje; y aplicarás todo a un caso concreto de Taller Nómada —una simulación de 200.000 combinaciones de asignación de tareas— para terminar haciendo lo único que zanja la discusión: medir si compensa. Adelanto la conclusión, porque es la lección más importante: casi nunca compensa, y Nómada Tareas no lo necesita.

Contenido

  1. Qué es WebAssembly
  2. Qué NO es WebAssembly
  3. Por qué existe: rendimiento predecible y código reutilizable
  4. El modelo de ejecución: módulo, instancia, memoria y tabla
  5. Por qué solo entiende números
  6. El formato de texto .wat frente al binario .wasm
  7. Cargar e instanciar desde JavaScript
  8. Cruzar la frontera: exportar, importar y compartir memoria
  9. Las herramientas reales: Rust, C/C++ y AssemblyScript
  10. Casos de uso reales
  11. JavaScript frente a Wasm: comparativa honesta
  12. WASI y el modelo de componentes
  13. El caso de Taller Nómada: la planificación pesada
  14. Medir si compensa
  15. Errores Comunes y Consejos
  16. Ejercicios
  17. Conclusión

  1. Qué es WebAssembly

WebAssembly es un formato de instrucciones binario para una máquina virtual de pila. Cuatro adjetivos lo definen:

  • Binario: no es texto que se analiza como JavaScript, sino bytes con una estructura fija que el navegador puede validar y compilar muy rápido.
  • Portable: el mismo .wasm funciona igual en cualquier navegador, sistema operativo y arquitectura de procesador.
  • Seguro: se ejecuta en la misma caja de arena que JavaScript, sin acceso al sistema de ficheros, a la red ni a la memoria del proceso salvo lo que le des explícitamente.
  • Rápido: se compila a código máquina y alcanza velocidades cercanas a las nativas.

La idea clave, y la que más se malinterpreta:

WebAssembly no reemplaza a JavaScript. Se ejecuta en el mismo motor, junto a JavaScript, y ambos se llaman mutuamente. Es un compañero, no un sustituto.

En un navegador moderno, tu app.js y un módulo .wasm conviven en la misma pestaña, comparten el mismo hilo (salvo que uses workers) y se invocan como funciones normales.

Es además un estándar del W3C desde 2019, con soporte en todos los navegadores modernos desde 2017. No es experimental.

  1. Qué NO es WebAssembly

Esta sección importa más que la anterior, porque casi todos los malentendidos vienen de aquí.

Mito Realidad
«Es el sustituto de JavaScript» Conviven. Wasm ni siquiera puede tocar el DOM sin pasar por JavaScript
«Todo es más rápido en Wasm» Solo el cálculo intensivo. Manipular el DOM o hacer peticiones es igual o más lento por el coste de cruzar la frontera
«Sirve para ocultar mi código» El binario se descarga igual y se descompila con herramientas públicas. No es ofuscación ni protección
«Puedo acceder al sistema de ficheros» No en el navegador. Está en la misma caja de arena, y solo hace lo que JavaScript le permita
«Es un lenguaje de programación» Es un formato de destino de compilación. Nadie lo escribe a mano salvo para aprender
«Sirve para cualquier aplicación web» Para la enorme mayoría —incluida Nómada Tareas— no aporta nada

Y la limitación práctica más relevante: Wasm no tiene acceso directo al DOM. Si un módulo quiere cambiar el texto de una tarjeta, tiene que llamar a una función JavaScript que tú le hayas pasado como importación. Eso significa que una aplicación cuyo trabajo principal sea pintar interfaz —como la nuestra— no gana nada, porque el cuello de botella no está en el cálculo.

  1. Por qué existe: rendimiento predecible y código reutilizable

Wasm nació para resolver dos problemas concretos.

Rendimiento predecible. Los motores de JavaScript son extraordinariamente rápidos gracias a la compilación just-in-time: analizan el código en ejecución, deducen los tipos, generan código máquina optimizado. Pero esa optimización es especulativa, y puede deshacerse:

function sumar(a, b) { return a + b; }

sumar(1, 2);            // el motor optimiza asumiendo enteros
sumar(1.5, 2.5);        // sigue bien: números
sumar('a', 'b');        // ← DESOPTIMIZACIÓN: había asumido números, ahora hay cadenas

Ese fenómeno se llama deoptimization, y hace que el rendimiento de JavaScript sea excelente pero variable. WebAssembly, en cambio, tiene tipos estáticos, sin recolector de basura en su núcleo y sin especulación: su rendimiento es predecible, que en un motor de juego a 60 fps importa más que la velocidad media.

Reutilizar código existente. Hay décadas de bibliotecas escritas en C y C++ —códecs de vídeo, criptografía, motores físicos, procesamiento de imagen, SQLite— que representan millones de horas de trabajo. Reescribirlas en JavaScript sería absurdo. Compilarlas a Wasm las trae a la web tal cual.

flowchart LR
    subgraph Fuentes
        R["Rust"]
        C["C / C++"]
        A["AssemblyScript"]
        G["Go, Zig, Swift…"]
    end
    R --> W["módulo .wasm"]
    C --> W
    A --> W
    G --> W
    W --> M["Motor del navegador<br/>(el mismo que ejecuta JS)"]
    JS["JavaScript"] --> M
    M --> CPU["Código máquina"]

  1. El modelo de ejecución: módulo, instancia, memoria y tabla

Cuatro conceptos, y conviene distinguirlos bien porque los nombres se parecen.

Concepto Qué es Analogía en JavaScript
Módulo (WebAssembly.Module) El código compilado, sin estado. Se puede reutilizar y cachear Una clase
Instancia (WebAssembly.Instance) Un módulo con su memoria y su estado, listo para ejecutar Un objeto creado con new
Memoria (WebAssembly.Memory) Un bloque de bytes contiguo y redimensionable Un ArrayBuffer gigante
Tabla (WebAssembly.Table) Un array de referencias a funciones Un array de funciones, para llamadas indirectas

La memoria lineal es la pieza central y la más ajena a quien viene de JavaScript. Es literalmente un ArrayBuffer: una tira de bytes numerados desde 0, sin estructura. No hay objetos, ni cadenas, ni arrays: solo bytes que el módulo interpreta según el tipo que espera en cada posición.

flowchart TB
    subgraph Pagina["Pestaña del navegador"]
        JS["JavaScript<br/>objetos, cadenas, GC"]
        subgraph Inst["Instancia Wasm"]
            F["Funciones exportadas"]
            MEM["Memoria lineal<br/>(ArrayBuffer de bytes)"]
            TAB["Tabla de funciones"]
        end
    end
    JS -->|"llama a exports.f(3, 4)"| F
    F -->|"lee y escribe"| MEM
    JS <-->|"new Uint8Array(memory.buffer)"| MEM
    F -->|"llama a imports.avisar()"| JS

Lo importante de ese diagrama: JavaScript puede leer y escribir la memoria de Wasm directamente, mediante vistas tipadas (Uint8Array, Float64Array…) sobre el mismo ArrayBuffer. No hay copia. Ese acceso compartido es la única forma de pasar datos grandes de un lado a otro con eficiencia.

Wasm tiene solo cuatro tipos numéricos en su versión base: i32, i64, f32 y f64 (enteros y decimales de 32 y 64 bits). Nada más. Ni booleanos, ni caracteres, ni punteros de verdad: un puntero es simplemente un i32 que representa un desplazamiento dentro de la memoria lineal.

  1. Por qué solo entiende números

De lo anterior se sigue la consecuencia más práctica de toda la lección: pasar una cadena a Wasm no es pasar una cadena. Hay que serializarla.

Para enviar 'Presupuesto de la carpintería' a un módulo hay que: codificarla a bytes UTF-8, reservar espacio en la memoria lineal, escribir esos bytes ahí, y pasar a la función el desplazamiento y la longitud, dos números.

/** Escribe una cadena en la memoria de Wasm y devuelve dónde y cuánto ocupa. */
function escribirCadena(instancia, texto) {
  const bytes = new TextEncoder().encode(texto);              // cadena → bytes UTF-8

  // El módulo debe exportar un asignador; aquí suponemos uno simple
  const puntero = instancia.exports.reservar(bytes.length);

  const memoria = new Uint8Array(instancia.exports.memory.buffer);
  memoria.set(bytes, puntero);                                // copia byte a byte

  return { puntero, longitud: bytes.length };
}

/** Y el camino de vuelta */
function leerCadena(instancia, puntero, longitud) {
  const memoria = new Uint8Array(instancia.exports.memory.buffer, puntero, longitud);
  return new TextDecoder('utf-8').decode(memoria);
}

Ese ir y venir es el coste de cruzar la frontera, y explica por qué Wasm no siempre gana:

Qué pasas Coste
Un número (i32, f64) Casi cero: va directo
Un array de números Bajo, si escribes en la memoria compartida sin copiar
Una cadena Medio: codificar, reservar, copiar, y lo mismo a la vuelta
Un objeto o un array de objetos Alto: hay que aplanarlo a bytes en un formato acordado

De ahí la regla que gobierna el diseño de cualquier integración con Wasm:

Cruza la frontera pocas veces con mucho trabajo, nunca muchas veces con poco. Una llamada que procesa 200.000 elementos es excelente; 200.000 llamadas que procesan uno cada una son un desastre, y serán más lentas que hacerlo todo en JavaScript.

Un matiz para no quedarte con una idea desactualizada: la propuesta de referencias a tipos del host y la integración con el recolector de basura (WasmGC, ya disponible en varios navegadores) reducen esta fricción para lenguajes con GC como Java, Kotlin o Dart. Pero el modelo mental de la memoria lineal sigue siendo el que necesitas para C, C++ y Rust.

  1. El formato de texto .wat frente al binario .wasm

Wasm tiene dos representaciones equivalentes: el binario .wasm que se descarga, y un formato de texto .wat legible por humanos, pensado para depurar y aprender.

;; sumar.wat — el "hola mundo" de WebAssembly
(module
  ;; Declara una función llamada "sumar" que recibe dos i32 y devuelve un i32
  (func $sumar (param $a i32) (param $b i32) (result i32)
    local.get $a        ;; pone $a en la pila
    local.get $b        ;; pone $b en la pila
    i32.add)            ;; saca los dos, suma, deja el resultado en la pila

  ;; La hace visible desde JavaScript con el nombre "sumar"
  (export "sumar" (func $sumar))
)

Se lee de arriba abajo como una máquina de pila: cada instrucción saca operandos de la pila y deja resultados. local.get $a empuja el primer parámetro; local.get $b empuja el segundo; i32.add saca ambos y empuja la suma, que queda como valor de retorno.

Un ejemplo algo más real, con memoria:

(module
  ;; 1 página de memoria = 64 KiB. Se exporta para que JavaScript pueda leerla
  (memory (export "memory") 1)

  ;; Suma todos los f64 de un array que empieza en $ptr y tiene $n elementos
  (func $sumarArray (param $ptr i32) (param $n i32) (result f64)
    (local $i i32)
    (local $total f64)

    (loop $bucle
      (br_if 1 (i32.ge_u (local.get $i) (local.get $n)))    ;; si i >= n, salir

      ;; total += memoria[ptr + i*8]   (un f64 ocupa 8 bytes)
      (local.set $total
        (f64.add (local.get $total)
                 (f64.load (i32.add (local.get $ptr)
                                    (i32.mul (local.get $i) (i32.const 8))))))

      (local.set $i (i32.add (local.get $i) (i32.const 1)))
      (br $bucle)
    )
    (local.get $total)
  )
  (export "sumarArray" (func $sumarArray))
)

Fíjate en el nivel al que se trabaja: los índices se multiplican por 8 a mano porque un f64 ocupa ocho bytes, y el bucle se construye con saltos explícitos. Nadie escribe Wasm a mano para producción; se compila desde un lenguaje de alto nivel. Ver el .wat sirve para tres cosas: entender el modelo, depurar lo que generó tu compilador, y comprobar el tamaño del resultado.

Para convertir entre formatos se usa WABT (WebAssembly Binary Toolkit):

wat2wasm sumar.wat -o sumar.wasm     # texto → binario
wasm2wat sumar.wasm -o sumar.wat     # binario → texto (¡se puede descompilar!)
wasm-objdump -x sumar.wasm           # inspeccionar secciones

Ese wasm2wat es la prueba de que Wasm no protege tu código: cualquiera puede recuperar una versión legible del módulo que sirves.

  1. Cargar e instanciar desde JavaScript

Hay cuatro formas de cargar un módulo, y una es claramente la mejor:

Función Entrada Devuelve Cuándo
WebAssembly.instantiateStreaming(respuesta, imports) Una Response de fetch { module, instance } La recomendada
WebAssembly.instantiate(bytes, imports) ArrayBuffer { module, instance } Si ya tienes los bytes
WebAssembly.compileStreaming(respuesta) Una Response Module Compilar ahora, instanciar luego
new WebAssembly.Instance(module, imports) Module Instance Síncrono: solo para módulos pequeños ya compilados
// js/wasm/cargar.js
export async function cargarModulo(url, importaciones = {}) {
  // instantiateStreaming compila MIENTRAS se descarga: no espera al último byte
  const { instance, module } = await WebAssembly.instantiateStreaming(
    fetch(url),
    importaciones
  );
  return { exports: instance.exports, module };
}
const { exports } = await cargarModulo('/wasm/sumar.wasm');
console.log(exports.sumar(3, 4));      // 7   ← se llama como una función normal

instantiateStreaming es la forma correcta porque compila en paralelo a la descarga, en lugar de esperar a tenerlo todo. Pero tiene un requisito estricto que provoca el error más frecuente de todos:

El servidor debe enviar Content-Type: application/wasm. Si no, el navegador lanza TypeError: Incorrect response MIME type.

Muchos servidores estáticos ya lo hacen; algunos no. El respaldo, por si acaso:

export async function cargarModuloRobusto(url, importaciones = {}) {
  try {
    return await WebAssembly.instantiateStreaming(fetch(url), importaciones);
  } catch (error) {
    console.warn('[wasm] Streaming falló, cargando por ArrayBuffer:', error.message);
    const respuesta = await fetch(url);
    if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status}`);   // el ok de 07-02
    const bytes = await respuesta.arrayBuffer();
    return WebAssembly.instantiate(bytes, importaciones);
  }
}

Y comprobar disponibilidad, con el mismo patrón de mejora progresiva de 07-06:

export const HAY_WASM = typeof WebAssembly === 'object'
  && typeof WebAssembly.instantiateStreaming === 'function';

  1. Cruzar la frontera: exportar, importar y compartir memoria

Exportar es lo que el módulo ofrece a JavaScript: funciones, memoria, tablas y variables globales. Todo aparece en instance.exports.

console.log(Object.keys(instancia.exports));
// ['memory', 'sumar', 'sumarArray', 'reservar']

Importar es lo que JavaScript ofrece al módulo. Se pasa como segundo argumento, agrupado por espacios de nombres:

const importaciones = {
  env: {
    // El módulo puede llamar a funciones nuestras
    avisar: (codigo) => console.log('[wasm] aviso', codigo),
    ahora: () => Date.now(),

    // O usar una memoria que creamos nosotros
    memory: new WebAssembly.Memory({ initial: 16, maximum: 256 })   // páginas de 64 KiB
  }
};

const { instance } = await WebAssembly.instantiateStreaming(fetch('/wasm/plan.wasm'), importaciones);

Ahí está la respuesta a «¿cómo toca Wasm el DOM?»: no lo toca. Le pasas una función JavaScript que sí puede, y el módulo la llama. Todo lo que Wasm puede hacer con el mundo exterior pasa por las importaciones que tú le des, y eso es exactamente lo que lo hace seguro.

Compartir memoria es la forma eficiente de mover datos en volumen:

/** Copia un array de JavaScript a la memoria de Wasm y llama a la función. */
function calcularSobreArray(instancia, valores) {
  const { memory, reservar, sumarArray } = instancia.exports;

  const bytes = valores.length * 8;                    // Float64Array: 8 bytes por número
  const puntero = reservar(bytes);

  // Una VISTA sobre la memoria de Wasm: no hay copia del buffer, solo interpretación
  const vista = new Float64Array(memory.buffer, puntero, valores.length);
  vista.set(valores);                                  // aquí sí se copian los datos

  return sumarArray(puntero, valores.length);
}

Y una trampa que cuesta horas descubrir:

Si la memoria crece (por una llamada a memory.grow() o por una reserva interna del módulo), el ArrayBuffer anterior queda desvinculado y todas tus vistas dejan de funcionar. Crea la vista justo antes de usarla, nunca la guardes en una variable de larga vida.

// ✗ Peligroso: si la memoria crece, esta vista queda inservible
const MEMORIA = new Uint8Array(instancia.exports.memory.buffer);

// ✓ Crear la vista en el momento de usarla
const vista = () => new Uint8Array(instancia.exports.memory.buffer);

  1. Las herramientas reales: Rust, C/C++ y AssemblyScript

En la práctica nadie escribe .wat. Se compila desde un lenguaje de alto nivel, y cada uno tiene su cadena de herramientas.

Herramienta Lenguaje Fuerte en Curva Tamaño típico
wasm-pack + wasm-bindgen Rust La mejor integración con JavaScript; genera el pegamento automáticamente Alta (aprender Rust) Pequeño (decenas de KB)
Emscripten C / C++ Portar código existente, incluidas bibliotecas enormes Media Medio a grande
AssemblyScript Subconjunto de TypeScript Empezar sin aprender otro lenguaje Baja Muy pequeño
TinyGo Go Reutilizar código Go Media Medio
wasm-bindgen (solo) Rust Control fino de los enlaces Alta

Rust con wasm-pack es la opción más completa hoy. wasm-bindgen genera automáticamente el código JavaScript que traduce cadenas, structs y objetos, ahorrándote toda la gestión manual de la memoria:

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

#[wasm_bindgen]
pub fn mejor_asignacion(horas: &[f64], capacidad: f64) -> f64 {
    // ¡Aquí sí puedes usar cadenas y structs: wasm-bindgen genera el pegamento!
    horas.iter().filter(|h| **h <= capacidad).sum()
}
cargo install wasm-pack
wasm-pack build --target web        # genera pkg/ con el .wasm y el .js de enlace
import init, { mejor_asignacion } from './pkg/planificador.js';

await init();                        // carga e instancia el .wasm
console.log(mejor_asignacion(new Float64Array([12, 8, 5]), 10));   // 13

AssemblyScript es la mejor puerta de entrada para quien viene de JavaScript, porque se escribe con sintaxis de TypeScript:

// planificador.ts — AssemblyScript: parece TypeScript, compila a Wasm
export function esfuerzoPonderado(pesos: Int32Array, horas: Float64Array): f64 {
  let total: f64 = 0;
  for (let i = 0; i < pesos.length; i++) {
    total += f64(pesos[i]) * horas[i];
  }
  return total;
}
npm install --save-dev assemblyscript
npx asinit .
npx asc planificador.ts --target release --outFile build/planificador.wasm

Cuidado con una confusión habitual: AssemblyScript no es TypeScript. Comparte sintaxis, pero tiene tipos propios (i32, f64, u8), no tiene tipos dinámicos ni closures completos, y su recolector de basura es distinto. Es un lenguaje aparte con ropa familiar.

Emscripten compila C y C++ e incluso emula partes del sistema operativo (ficheros, SDL, OpenGL), lo que permite portar programas completos:

emcc planificador.c -O3 -s EXPORTED_FUNCTIONS='["_simular"]' \
     -s MODULARIZE=1 -o planificador.js

Es la herramienta que ha traído a la web cosas como ffmpeg o SQLite.

  1. Casos de uso reales

Wasm no es teórico: hay software importante funcionando con él.

Ámbito Ejemplos Por qué Wasm
Edición de imagen y vídeo Figma, Photoshop web, ffmpeg.wasm Millones de píxeles por fotograma; el cálculo domina
Bases de datos SQLite compilado a Wasm (sql.js, wa-sqlite) Motor completo, en C, funcionando en el navegador
Criptografía libsodium.js, implementaciones de hash Código auditado que no conviene reescribir; tiempo constante
Juegos y 3D Unity, Unreal, motores propios Rendimiento predecible a 60 fps, motores en C++
CAD e ingeniería AutoCAD web, simuladores Décadas de código C++ irreemplazable
Científico Pyodide (Python entero en el navegador), R, NumPy Ecosistemas completos sin servidor
Compiladores y lenguajes Playgrounds de Rust, Go, Zig Ejecutar el compilador real en el cliente
Fuera del navegador Plugins de Envoy, Fastly, Shopify Caja de arena portable y rápida para código de terceros

Ese último caso es interesante: Wasm empezó en el navegador, pero su combinación de portabilidad y aislamiento lo está convirtiendo en un formato de plugins para servidores y plataformas.

Fíjate en el patrón que comparten todos: cálculo intensivo sobre muchos datos, o código existente que no se puede reescribir. Ninguno es una aplicación de formularios y listas.

  1. JavaScript frente a Wasm: comparativa honesta

Aspecto JavaScript WebAssembly
Velocidad en cálculo intensivo Buena, con JIT Mejor y más predecible (1,2× a 3× típicamente)
Velocidad manipulando el DOM Nativa Peor: cada operación cruza la frontera
Tiempo hasta la primera ejecución Inmediato Descarga + compilación del módulo
Tamaño de descarga Solo tu código El módulo incluye su runtime: Rust ~30-100 KB, Emscripten a menudo más
Coste de llamar a una función Cero Bajo por llamada, alto si pasas cadenas u objetos
Depuración Excelente: DevTools completas Mejorando (mapas de fuentes), aún incómoda
Curva de aprendizaje Ya lo sabes Otro lenguaje + su cadena de herramientas
Ecosistema Enorme Pequeño y especializado
Acceso a APIs del navegador Total Solo lo que le importes desde JavaScript
Mantenimiento Un lenguaje Dos lenguajes, dos compilaciones, dos equipos

Las tres cosas que más se subestiman al evaluar Wasm:

  • El tamaño. Un módulo Rust sencillo son decenas de KB comprimidos; uno de Emscripten con biblioteca estándar puede ser cientos. Si tu cálculo tarda 40 ms en JavaScript y el módulo pesa 300 KB, descargarlo cuesta más de lo que ahorra.
  • El coste de la frontera. Si el módulo se llama muchas veces con poco trabajo, la traducción de datos se come la ganancia y acabas más lento.
  • El coste humano. Añadir Rust a un proyecto de JavaScript significa otra cadena de compilación, otras dependencias, otro conocimiento en el equipo y otro punto de fallo en el despliegue. Es una decisión de arquitectura, no un ajuste técnico.

Cuándo compensa de verdad, resumido en tres condiciones que deben darse a la vez:

  1. El trabajo es cálculo puro y prolongado (decenas de milisegundos o más).
  2. Se cruza la frontera pocas veces con muchos datos.
  3. O bien ya existe el código en C/C++/Rust y reescribirlo sería absurdo.

Si falta alguna, la respuesta correcta es JavaScript. Y si el problema es que la interfaz se congela, hay una alternativa mucho más barata que Wasm: mover el cálculo a un Web Worker, que sigue siendo JavaScript pero en otro hilo.

  1. WASI y el modelo de componentes

Dos piezas del futuro de Wasm que conviene conocer de nombre.

WASI (WebAssembly System Interface) es un conjunto estándar de APIs que da a los módulos Wasm acceso controlado a capacidades del sistema —ficheros, reloj, red, variables de entorno— fuera del navegador. Con WASI, un .wasm es un binario portable que corre igual en Linux, macOS y Windows, con permisos explícitos por capacidad. Es la base de plataformas como Wasmtime, Wasmer y de los plugins en el borde de la red.

El modelo de componentes aborda el problema del que hemos hablado todo el rato: la frontera. Define tipos de alto nivel —cadenas, registros, listas, variantes— mediante un lenguaje de interfaz (WIT), de modo que un componente escrito en Rust pueda llamar a otro escrito en Python sin que nadie escriba código de traducción a mano.

Ninguna de las dos cambia hoy tu forma de trabajar en el navegador, y por eso quedan aquí mencionadas. Pero explican hacia dónde va la tecnología: de «un formato para acelerar cálculos en la web» a «un formato universal, seguro y portable para ejecutar código de terceros en cualquier sitio».

  1. El caso de Taller Nómada: la planificación pesada

Vamos a construir un ejemplo honesto: un cálculo real de Nómada Tareas que es pesado, y a medir si Wasm compensa.

El problema. Marta quiere saber cómo repartir las tareas abiertas entre Iván, Lucía y ella para que nadie supere las 40 horas semanales (regla R7) minimizando el esfuerzo ponderado desequilibrado. Con 5 tareas abiertas y 3 personas hay 3⁵ = 243 combinaciones: trivial. Pero al planificar el trimestre entero, con 11 tareas y 3 personas, son 3¹¹ ≈ 177.000; y explorando también el orden dentro de cada persona, se pasa fácilmente de 200.000 combinaciones.

En JavaScript:

// js/planificacion/simulador.js — versión JavaScript, la referencia
import { PESOS } from '../util/formato.js';

const EQUIPO = ['Marta', 'Iván', 'Lucía'];
const MAX_HORAS = 40;                                  // R7

/**
 * Prueba todas las asignaciones posibles y devuelve la de menor desequilibrio.
 * Complejidad: 3^n. Con n = 11, unas 177.000 combinaciones.
 */
export function mejorReparto(tareas) {
  const n = tareas.length;
  const combinaciones = 3 ** n;

  let mejorCoste = Infinity;
  let mejorAsignacion = null;

  const horas = new Float64Array(3);
  const esfuerzo = new Float64Array(3);

  for (let c = 0; c < combinaciones; c += 1) {
    horas.fill(0);
    esfuerzo.fill(0);

    // Cada combinación se codifica en base 3: el dígito i dice a quién va la tarea i
    let resto = c;
    let valida = true;

    for (let i = 0; i < n; i += 1) {
      const persona = resto % 3;
      resto = Math.floor(resto / 3);

      horas[persona] += tareas[i].horasEstimadas;
      if (horas[persona] > MAX_HORAS) { valida = false; break; }     // poda por R7
      esfuerzo[persona] += PESOS[tareas[i].prioridad] * tareas[i].horasEstimadas;
    }
    if (!valida) continue;

    // Coste = diferencia entre quien más y quien menos esfuerzo carga
    const coste = Math.max(...esfuerzo) - Math.min(...esfuerzo);
    if (coste < mejorCoste) {
      mejorCoste = coste;
      mejorAsignacion = c;
    }
  }

  return { coste: mejorCoste, codigo: mejorAsignacion, combinaciones };
}

Este código es un buen candidato a Wasm porque cumple las tres condiciones: es cálculo puro, se llama una vez con todos los datos, y trabaja con números exclusivamente.

La versión en AssemblyScript, deliberadamente parecida:

// wasm/planificador.ts (AssemblyScript)
const MAX_HORAS: f64 = 40;

export function mejorReparto(horasPtr: usize, pesosPtr: usize, n: i32): f64 {
  let combinaciones: i32 = 1;
  for (let i = 0; i < n; i++) combinaciones *= 3;

  let mejorCoste: f64 = f64.MAX_VALUE;

  for (let c = 0; c < combinaciones; c++) {
    let h0: f64 = 0, h1: f64 = 0, h2: f64 = 0;
    let e0: f64 = 0, e1: f64 = 0, e2: f64 = 0;
    let resto = c;
    let valida = true;

    for (let i = 0; i < n; i++) {
      const persona = resto % 3;
      resto = resto / 3;

      // Lectura directa de la memoria lineal: 8 bytes por f64, 4 por i32
      const horas = load<f64>(horasPtr + i * 8);
      const peso  = f64(load<i32>(pesosPtr + i * 4));

      if (persona == 0)      { h0 += horas; if (h0 > MAX_HORAS) { valida = false; break; } e0 += peso * horas; }
      else if (persona == 1) { h1 += horas; if (h1 > MAX_HORAS) { valida = false; break; } e1 += peso * horas; }
      else                   { h2 += horas; if (h2 > MAX_HORAS) { valida = false; break; } e2 += peso * horas; }
    }
    if (!valida) continue;

    const max = Math.max(e0, Math.max(e1, e2));
    const min = Math.min(e0, Math.min(e1, e2));
    if (max - min < mejorCoste) mejorCoste = max - min;
  }
  return mejorCoste;
}

Y el puente desde JavaScript:

// js/planificacion/puente-wasm.js
let instancia = null;

export async function iniciarPlanificador(url = '/wasm/planificador.wasm') {
  if (instancia !== null) return instancia;                       // instanciar UNA vez
  const { instance } = await WebAssembly.instantiateStreaming(fetch(url), {
    env: { abort: () => { throw new Error('[wasm] abort'); } }
  });
  instancia = instance;
  return instancia;
}

export function mejorRepartoWasm(tareas) {
  const { memory, __new, mejorReparto } = instancia.exports;
  const n = tareas.length;

  // Reservar y escribir los dos arrays en la memoria lineal
  const horasPtr = __new(n * 8, 0);
  const pesosPtr = __new(n * 4, 0);

  // Vistas creadas AHORA: la memoria pudo crecer al reservar
  new Float64Array(memory.buffer, horasPtr, n).set(tareas.map((t) => t.horasEstimadas));
  new Int32Array(memory.buffer, pesosPtr, n).set(tareas.map((t) => PESOS[t.prioridad]));

  return mejorReparto(horasPtr, pesosPtr, n);                     // UNA sola travesía
}

Observa lo que se ha logrado: se cruza la frontera una vez para escribir los datos y una vez para llamar. Todo el trabajo de 177.000 iteraciones ocurre dentro de Wasm. Es el diseño correcto.

  1. Medir si compensa

Y ahora lo único que zanja la discusión. Cualquier afirmación sobre rendimiento sin medición es una opinión.

// js/planificacion/comparar.js
import { mejorReparto } from './simulador.js';
import { iniciarPlanificador, mejorRepartoWasm } from './puente-wasm.js';

/** Ejecuta una función varias veces y devuelve la mediana, más robusta que la media. */
function medir(nombre, funcion, repeticiones = 7) {
  const tiempos = [];
  funcion();                                          // calentamiento: deja que el JIT optimice

  for (let i = 0; i < repeticiones; i += 1) {
    const inicio = performance.now();
    funcion();
    tiempos.push(performance.now() - inicio);
  }
  tiempos.sort((a, b) => a - b);
  const mediana = tiempos[Math.floor(tiempos.length / 2)];
  console.log(`${nombre}: ${mediana.toFixed(1)} ms (mín ${tiempos[0].toFixed(1)})`);
  return mediana;
}

export async function comparar(tareas) {
  const inicioCarga = performance.now();
  await iniciarPlanificador();
  const msCarga = performance.now() - inicioCarga;

  const msJs   = medir('JavaScript', () => mejorReparto(tareas));
  const msWasm = medir('WebAssembly', () => mejorRepartoWasm(tareas));

  const ahorroPorLlamada = msJs - msWasm;
  const llamadasParaAmortizar = ahorroPorLlamada > 0 ? Math.ceil(msCarga / ahorroPorLlamada) : Infinity;

  console.table({
    'Carga del módulo (ms)': msCarga.toFixed(1),
    'JavaScript (ms)': msJs.toFixed(1),
    'WebAssembly (ms)': msWasm.toFixed(1),
    'Aceleración': `${(msJs / msWasm).toFixed(2)}×`,
    'Llamadas para amortizar la carga': llamadasParaAmortizar
  });

  return { msCarga, msJs, msWasm };
}

Un resultado típico en un portátil moderno con 11 tareas:

Medida Valor
Carga y compilación del módulo ~15 ms (+ 24 KB de descarga)
JavaScript ~48 ms
WebAssembly ~19 ms
Aceleración 2,5×
Llamadas necesarias para amortizar la carga 1

La aceleración es real. Y ahora la pregunta honesta: ¿compensa para Nómada Tareas?

Pregunta Respuesta
¿Es un cálculo pesado? Sí, 177.000 combinaciones
¿Se llama a menudo? No: Marta planifica una vez por trimestre
¿48 ms molestan al usuario? No. Por debajo de ~100 ms se percibe como instantáneo
¿Cuánto cuesta mantenerlo? Un lenguaje más, una cadena de compilación más, un despliegue más
¿Hay alternativa más barata? Sí: un Web Worker, que evita el bloqueo sin salir de JavaScript

Veredicto: no compensa. Ahorrar 29 milisegundos en una operación trimestral no justifica añadir AssemblyScript al proyecto. Si el cálculo creciera a 20 tareas (3²⁰ ≈ 3.500 millones de combinaciones), ni Wasm salvaría el enfoque: haría falta un algoritmo mejor —programación dinámica o una heurística—, y esa es la lección de fondo.

Antes de optimizar la tecnología, optimiza el algoritmo. Y antes de optimizar nada, mide. Un cambio de O(3ⁿ) a O(n log n) supera a cualquier factor constante de 2,5×.

Esa disciplina —medir primero, decidir después, con criterios de coste y no de entusiasmo— es exactamente el tema del Módulo 9, y en particular de Medir Antes de Optimizar, donde encontrarás las herramientas de perfilado que convierten estas mediciones caseras en análisis serios.

Errores Comunes y Consejos

  • Creer que Wasm sustituye a JavaScript. Conviven, y Wasm ni siquiera toca el DOM sin pasar por JavaScript.
  • Usar Wasm para acelerar manipulación del DOM. Será más lento: cada operación cruza la frontera.
  • Usar Wasm para ocultar el código. wasm2wat lo descompila. No es protección.
  • Cruzar la frontera en un bucle. 200.000 llamadas pequeñas son más lentas que hacerlo todo en JavaScript. Una llamada con 200.000 elementos es lo correcto.
  • Servir el .wasm sin Content-Type: application/wasm. instantiateStreaming falla con un error de MIME desconcertante.
  • Guardar una vista tipada de larga vida sobre la memoria. Si la memoria crece, el ArrayBuffer queda desvinculado y la vista deja de servir. Créala justo antes de usarla.
  • Olvidar liberar memoria. En C/C++/Rust sin wasm-bindgen, lo que reservas hay que liberarlo: hay fugas también en Wasm.
  • Instanciar el módulo en cada llamada. Compilar cuesta. Instancia una vez y reutiliza.
  • Ignorar el tamaño de descarga. Un módulo de 300 KB para ahorrar 20 ms es una pérdida neta.
  • Medir sin calentamiento. La primera ejecución de JavaScript no está optimizada por el JIT y falsea la comparación a favor de Wasm.
  • Medir una sola vez. Usa la mediana de varias ejecuciones.
  • Añadir Wasm sin plan de mantenimiento. Otro lenguaje, otra compilación, otro despliegue, otro conocimiento en el equipo.
  • Consejo: prueba primero un Web Worker. Si el problema es que la interfaz se congela, un worker lo resuelve sin salir de JavaScript.
  • Consejo: empieza por AssemblyScript si quieres experimentar. La sintaxis te resultará familiar y verás el modelo sin aprender Rust.
  • Consejo: mira el .wat de lo que genera tu compilador. Es la mejor forma de entender qué está pasando de verdad.
  • Consejo: en DevTools puedes poner puntos de interrupción en Wasm. La depuración ha mejorado mucho; con mapas de fuentes puedes incluso depurar el Rust original.
  • Consejo: comprueba disponibilidad (typeof WebAssembly === 'object') y ten siempre la ruta en JavaScript como respaldo.

Ejercicios

Ejercicio 1 — Cargar y usar un módulo mínimo. Escribe sumar.wat con dos funciones exportadas: sumar(a, b) que sume dos i32, y factorial(n) que calcule el factorial de forma iterativa. Compílalo con wat2wasm. Después escribe js/wasm/cargar.js con una función cargarModulo(url) que use instantiateStreaming con respaldo a arrayBuffer, compruebe la disponibilidad de WebAssembly y lance un error claro si el servidor no envía el MIME correcto. Verifica que sumar(3, 4) da 7 y factorial(10) da 3.628.800.

Ejercicio 2 — Comparativa rigurosa. Escribe compararImplementaciones({ nombre, js, wasm, entradas }) que ejecute ambas versiones sobre varios tamaños de entrada, con calentamiento, mediana de siete repeticiones y comprobación de que ambas devuelven el mismo resultado (una optimización que da un resultado distinto no es una optimización). Debe producir una tabla con console.table que incluya, por cada tamaño: tiempo JS, tiempo Wasm, aceleración y si los resultados coinciden. Añade el tamaño de entrada a partir del cual Wasm empieza a compensar.

Ejercicio 3 — Cuándo NO usar Wasm. Sin escribir Wasm, escribe un informe razonado —en forma de función evaluarWasm(perfil) que devuelva una recomendación— que reciba { msEnJs, llamadasPorSesion, kbDelModulo, tipoDeTrabajo, existeCodigoNativo, equipoConoceRust } y devuelva { recomendacion: 'si' | 'no' | 'quizas', motivos: [...] }. Debe aplicar las reglas de la lección: descartar si el trabajo es DOM o E/S, descartar si el tiempo en JavaScript ya es imperceptible, calcular cuántas sesiones tarda en amortizarse la descarga, y valorar el coste humano. Pruébalo con el perfil real de Nómada Tareas y con el de un editor de imagen.

Soluciones

Solución 1

;; sumar.wat
(module
  (func $sumar (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)

  (func $factorial (param $n i32) (result i32)
    (local $resultado i32)
    (local $i i32)
    (local.set $resultado (i32.const 1))
    (local.set $i (i32.const 2))

    (block $fin
      (loop $bucle
        (br_if $fin (i32.gt_s (local.get $i) (local.get $n)))    ;; si i > n, salir
        (local.set $resultado (i32.mul (local.get $resultado) (local.get $i)))
        (local.set $i (i32.add (local.get $i) (i32.const 1)))
        (br $bucle)))

    (local.get $resultado))

  (export "sumar" (func $sumar))
  (export "factorial" (func $factorial))
)
wat2wasm sumar.wat -o sumar.wasm
// js/wasm/cargar.js
export const HAY_WASM = typeof WebAssembly === 'object'
  && typeof WebAssembly.instantiate === 'function';

export async function cargarModulo(url, importaciones = {}) {
  if (!HAY_WASM) throw new Error('Este navegador no soporta WebAssembly.');

  // Camino rápido: compila mientras descarga
  if (typeof WebAssembly.instantiateStreaming === 'function') {
    try {
      const { instance, module } = await WebAssembly.instantiateStreaming(fetch(url), importaciones);
      return { exports: instance.exports, module };
    } catch (error) {
      if (error.message.includes('MIME')) {
        console.warn(`[wasm] El servidor no envía Content-Type: application/wasm para ${url}. ` +
                     'Se usa el camino lento; corrígelo en el servidor.');
      } else {
        console.warn('[wasm] instantiateStreaming falló:', error.message);
      }
    }
  }

  // Respaldo: descargar entero y compilar después
  const respuesta = await fetch(url);
  if (!respuesta.ok) throw new Error(`HTTP ${respuesta.status} al cargar ${url}`);   // 07-02
  const bytes = await respuesta.arrayBuffer();
  const { instance, module } = await WebAssembly.instantiate(bytes, importaciones);
  return { exports: instance.exports, module };
}
const { exports } = await cargarModulo('/wasm/sumar.wasm');
console.log(exports.sumar(3, 4));        // 7
console.log(exports.factorial(10));      // 3628800

El detalle valioso es distinguir el error de MIME de cualquier otro fallo: el mensaje dice exactamente qué hay que arreglar en el servidor en lugar de dejar un TypeError opaco. Y fíjate en el factorial: con i32 desborda a partir de 13, un recordatorio de que los tipos de Wasm son de tamaño fijo y no avisan al desbordar, a diferencia de los Number de JavaScript.

Solución 2

// js/planificacion/comparar.js
function medirMediana(funcion, repeticiones = 7) {
  funcion();                                          // calentamiento del JIT
  const tiempos = [];
  for (let i = 0; i < repeticiones; i += 1) {
    const t0 = performance.now();
    funcion();
    tiempos.push(performance.now() - t0);
  }
  tiempos.sort((a, b) => a - b);
  return tiempos[Math.floor(tiempos.length / 2)];
}

const casiIguales = (a, b, epsilon = 1e-9) =>
  typeof a === 'number' && typeof b === 'number'
    ? Math.abs(a - b) < epsilon
    : JSON.stringify(a) === JSON.stringify(b);

export function compararImplementaciones({ nombre, js, wasm, entradas }) {
  const filas = {};
  let umbral = null;

  for (const entrada of entradas) {
    const resultadoJs   = js(entrada.datos);
    const resultadoWasm = wasm(entrada.datos);
    const coinciden = casiIguales(resultadoJs, resultadoWasm);

    if (!coinciden) {
      console.error(`❌ ${nombre} · ${entrada.etiqueta}: resultados distintos`,
                    { js: resultadoJs, wasm: resultadoWasm });
    }

    const msJs   = medirMediana(() => js(entrada.datos));
    const msWasm = medirMediana(() => wasm(entrada.datos));
    const aceleracion = msJs / msWasm;

    if (umbral === null && aceleracion > 1.2) umbral = entrada.etiqueta;

    filas[entrada.etiqueta] = {
      'JS (ms)': msJs.toFixed(2),
      'Wasm (ms)': msWasm.toFixed(2),
      'Aceleración': `${aceleracion.toFixed(2)}×`,
      'Coinciden': coinciden ? '✅' : '❌'
    };
  }

  console.table(filas);
  console.log(umbral
    ? `Wasm empieza a compensar a partir de: ${umbral}`
    : 'Wasm no compensa en ningún tamaño probado.');
  return filas;
}

La comprobación de que los resultados coinciden es la parte que nadie escribe y que más falta hace: una versión más rápida que devuelve otra cosa no es una optimización, es un bug. Y el casiIguales con epsilon reconoce una realidad de los decimales de coma flotante: JavaScript y Wasm pueden diferir en el último bit al acumular sumas en distinto orden.

Solución 3

export function evaluarWasm({
  msEnJs, llamadasPorSesion, kbDelModulo,
  tipoDeTrabajo,                  // 'calculo' | 'dom' | 'red' | 'mixto'
  existeCodigoNativo = false,
  equipoConoceRust = false
}) {
  const motivos = [];

  // 1 · Descartes inmediatos
  if (tipoDeTrabajo === 'dom' || tipoDeTrabajo === 'red') {
    return { recomendacion: 'no', motivos: [
      `El trabajo es de tipo "${tipoDeTrabajo}": Wasm no puede acelerarlo y el coste de cruzar la frontera lo empeoraría.`
    ]};
  }

  if (msEnJs < 50) {
    motivos.push(`${msEnJs} ms en JavaScript ya se percibe como instantáneo (umbral ~100 ms).`);
  }

  // 2 · Amortización de la descarga (a ~1 ms por KB en una conexión mediana)
  const msDescarga = kbDelModulo * 1;
  const ahorroEstimado = msEnJs * 0.6;                  // suponemos una aceleración de 2,5×
  const llamadasParaAmortizar = Math.ceil(msDescarga / ahorroEstimado);

  motivos.push(
    `El módulo (${kbDelModulo} KB ≈ ${msDescarga} ms de descarga) se amortiza tras ` +
    `${llamadasParaAmortizar} llamadas; la sesión típica hace ${llamadasPorSesion}.`
  );

  // 3 · Factores humanos y de reutilización
  if (existeCodigoNativo) motivos.push('Ya existe código C/C++/Rust: reescribirlo sería el mayor coste.');
  if (!equipoConoceRust && !existeCodigoNativo) {
    motivos.push('El equipo no domina un lenguaje compilable: hay coste de aprendizaje y de mantenimiento.');
  }
  motivos.push('Alternativa más barata: mover el cálculo a un Web Worker (sigue siendo JavaScript).');

  // 4 · Veredicto
  if (existeCodigoNativo) return { recomendacion: 'si', motivos };
  if (msEnJs < 50 || llamadasPorSesion < llamadasParaAmortizar) {
    return { recomendacion: 'no', motivos };
  }
  if (msEnJs > 200 && llamadasPorSesion > llamadasParaAmortizar * 5) {
    return { recomendacion: 'si', motivos };
  }
  return { recomendacion: 'quizas', motivos };
}
// El caso real de Nómada Tareas
console.log(evaluarWasm({
  msEnJs: 48, llamadasPorSesion: 1, kbDelModulo: 24,
  tipoDeTrabajo: 'calculo', existeCodigoNativo: false, equipoConoceRust: false
}));
// { recomendacion: 'no', motivos: [ '48 ms en JavaScript ya se percibe como instantáneo…', … ] }

// Un editor de imagen en el navegador
console.log(evaluarWasm({
  msEnJs: 2400, llamadasPorSesion: 300, kbDelModulo: 180,
  tipoDeTrabajo: 'calculo', existeCodigoNativo: true, equipoConoceRust: true
}));
// { recomendacion: 'si', motivos: [ 'Ya existe código C/C++/Rust…', … ] }

Lo interesante de esta función no es el código, sino que obliga a escribir los números antes de decidir. La mayoría de las decisiones de «vamos a usar Wasm» se toman por entusiasmo tecnológico y no sobreviven a rellenar honestamente estos seis campos.

Conclusión

Cierras el Módulo 7 con la lección más contraintuitiva de todas: la que enseña una tecnología y termina recomendando no usarla. Sabes que WebAssembly es un formato binario portable y seguro que se ejecuta en el mismo motor que JavaScript, y que no lo sustituye: conviven, se llaman mutuamente y Wasm ni siquiera puede tocar el DOM sin que le pases una función JavaScript como importación. Sabes desmontar los cuatro mitos —no es más rápido en todo, no oculta el código (wasm2wat lo descompila), no accede al sistema de ficheros en el navegador, y no es un lenguaje sino un destino de compilación—. Y conoces las dos razones reales por las que existe: el rendimiento predecible, sin las desoptimizaciones especulativas del JIT, y la posibilidad de reutilizar décadas de código en C, C++ y Rust.

Entiendes el modelo de ejecución: módulo como clase e instancia como objeto, la memoria lineal que es literalmente un ArrayBuffer compartido con JavaScript, la tabla de funciones, y los cuatro únicos tipos numéricos. De ahí sale la idea que gobierna cualquier integración: Wasm solo entiende números, las cadenas y los objetos hay que serializarlos a bytes, y por tanto hay que cruzar la frontera pocas veces con mucho trabajo, nunca al revés. Sabes leer un .wat como una máquina de pila, cargar un módulo con instantiateStreaming —con su exigencia de Content-Type: application/wasm y su respaldo por arrayBuffer—, exportar e importar, y compartir memoria con vistas tipadas creadas justo antes de usarlas, porque si la memoria crece el buffer anterior queda desvinculado.

Conoces las herramientas de verdad —wasm-pack con wasm-bindgen para Rust, Emscripten para C y C++, AssemblyScript como puerta de entrada desde TypeScript—, los casos donde Wasm ha ganado de forma indiscutible (Figma, ffmpeg.wasm, SQLite, Pyodide, motores de juego, CAD) y la comparativa honesta que casi nadie hace: el tamaño de descarga, el coste de la frontera y el coste humano de mantener dos lenguajes y dos cadenas de compilación. Y sobre todo, has hecho el ejercicio completo con la planificación de Taller Nómada: una simulación real de 177.000 combinaciones, una versión en JavaScript, otra en AssemblyScript, una medición con calentamiento y mediana… y un veredicto razonado de no compensa, porque ahorrar 29 milisegundos en una operación trimestral no justifica añadir un lenguaje al proyecto, y porque cuando el problema crece de verdad la respuesta no es cambiar de tecnología sino cambiar de algoritmo.

Con esto termina el Módulo 7. Nómada Tareas ha recorrido un camino completo: recuerda entre recargas con la Web Storage API y su repositorio local; habla con un servidor mediante fetch, comprobando siempre respuesta.ok; sobrevive a la red real con ErrorDeApi, AbortController, timeouts, reintentos con retroceso exponencial y una interfaz de cuatro estados; se sincroniza en vivo con un CanalTablero sobre WebSockets que se reconecta solo, late y encola lo pendiente; funciona sin conexión e instalada gracias a un service worker con su app shell precacheado y sus estrategias por tipo de recurso; y se comporta como una aplicación cuidada, con filtros enlazables, tema respetuoso con el sistema, animaciones que atienden a prefers-reduced-motion y formatos en español de verdad con Intl. La capa js/datos/ que empezó vacía tiene ahora cuatro módulos, y el modelo de los Módulos 1 a 5 no ha cambiado ni una línea en todo el proceso.

Y ahí está exactamente el problema que abre el siguiente módulo. La aplicación hace ya muchísimas cosas: seis capas, catorce módulos, dos fuentes de datos, un canal en tiempo real, un proxy de caché, reglas de negocio, estados de interfaz, reversiones optimistas y colas de sincronización. Cada una de esas piezas puede romper a las demás, y ninguna comprobación se hace sola. Ahora mismo, la única forma de saber si algo sigue funcionando es abrirlo y probarlo a mano; y cuando falla, la única forma de averiguar por qué es sembrar el código de console.log. Eso no escala: llega un punto en el que cambiar una línea da miedo. Hace falta depurar con método en lugar de por intuición, mantener el código consistente con herramientas automáticas, y sobre todo automatizar las pruebas para que la máquina compruebe en segundos lo que tú no puedes comprobar en una tarde. Ese es el Módulo 8: Pruebas y Depuración, que empieza por Depuración de JavaScript —y donde comprobarás que aquella frontera limpia entre modelo y vista, la que llevas seis módulos manteniendo, era desde el principio lo que iba a hacer posible probarlo todo sin abrir un navegador—.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados