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
- Qué es WebAssembly
- Qué NO es WebAssembly
- Por qué existe: rendimiento predecible y código reutilizable
- El modelo de ejecución: módulo, instancia, memoria y tabla
- Por qué solo entiende números
- El formato de texto
.watfrente al binario.wasm - Cargar e instanciar desde JavaScript
- Cruzar la frontera: exportar, importar y compartir memoria
- Las herramientas reales: Rust, C/C++ y AssemblyScript
- Casos de uso reales
- JavaScript frente a Wasm: comparativa honesta
- WASI y el modelo de componentes
- El caso de Taller Nómada: la planificación pesada
- Medir si compensa
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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
.wasmfunciona 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.
- 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.
- 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 cadenasEse 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"]
- 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.
- 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.
- El formato de texto
.wat frente al binario .wasm
.wat frente al binario .wasmWasm 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 seccionesEse wasm2wat es la prueba de que Wasm no protege tu código: cualquiera puede recuperar una versión legible del módulo que sirves.
- 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 normalinstantiateStreaming 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 lanzaTypeError: 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';
- 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.
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), elArrayBufferanterior 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);
- 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()
}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)); // 13AssemblyScript 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.wasmCuidado 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:
Es la herramienta que ha traído a la web cosas como ffmpeg o SQLite.
- 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.
- 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:
- El trabajo es cálculo puro y prolongado (decenas de milisegundos o más).
- Se cruza la frontera pocas veces con muchos datos.
- 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.
- 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».
- 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 sí 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.
- 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.
wasm2watlo 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
.wasmsinContent-Type: application/wasm.instantiateStreamingfalla con un error de MIME desconcertante. - Guardar una vista tipada de larga vida sobre la memoria. Si la memoria crece, el
ArrayBufferqueda 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
.watde 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))
)// 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)); // 3628800El 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
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
