En la lección anterior descubriste que libuv mantiene un ciclo que pregunta sin descanso "¿hay algo terminado?, ¿hay algún callback que ejecutar?". Ese ciclo es el bucle de eventos, y es la pieza que convierte a Node.js en lo que es. Todo lo demás —los callbacks, las promesas, los eventos, el servidor HTTP, la base de datos— está construido encima.
Esta es, sin exageración, la lección más importante del curso. No porque vayas a escribir código que manipule el bucle directamente (no lo harás casi nunca), sino porque el orden en que se ejecutan las cosas en Node solo tiene sentido cuando conoces sus fases. Sin ese conocimiento, tarde o temprano te encontrarás mirando una salida por consola que aparece "en el orden equivocado", un temporizador que se dispara tarde, o un servidor que responde bien en tu portátil y mal en producción, y no tendrás modelo mental para explicarlo.
Al terminar podrás predecir línea a línea la salida de cualquier programa asíncrono en Node, sabrás cuándo setImmediate gana a setTimeout y cuándo no, entenderás por qué process.nextTick se cuela delante de todo el mundo y por qué eso puede ser peligroso, y sabrás medir la métrica de salud número uno de un servidor Node: el retraso del bucle de eventos.
Contenido
- Qué es exactamente el bucle de eventos
- Las seis fases, en orden
- Fase 1: timers
- Fase 2: pending callbacks
- Fase 3: idle y prepare
- Fase 4: poll, el corazón del corazón
- Fase 5: check y
setImmediate - Fase 6: close callbacks
setTimeoutfrente asetImmediate- Las dos colas que se cuelan:
process.nextTicky las microtareas - Una traza completa que debes poder predecir
- La inanición del bucle: el peligro de
nextTick setTimeout(fn, 0)no es inmediato- Medir el retraso del bucle de eventos
- Escena Viva: por qué un cálculo síncrono degrada a todos
- Qué es exactamente el bucle de eventos
El bucle de eventos es un bucle while escrito en C dentro de libuv. Nada más místico que eso. Su cuerpo, muy simplificado, se parece a esto:
/* Pseudocódigo de uv_run(), en libuv */
while (hay_tareas_pendientes(loop)) {
ejecutar_fase_timers(loop);
ejecutar_fase_pending_callbacks(loop);
ejecutar_fase_idle_prepare(loop);
ejecutar_fase_poll(loop); /* aquí es donde se espera */
ejecutar_fase_check(loop);
ejecutar_fase_close_callbacks(loop);
}
/* Al salir del while, el proceso termina */Cada vuelta completa de ese while se llama tick o iteración del bucle. Y la condición hay_tareas_pendientes es exactamente la cuenta de referencias que viste en la lección anterior: mientras haya un temporizador programado, un socket abierto o una lectura de fichero en curso, el bucle sigue girando; cuando la cuenta llega a cero, el while termina y el proceso muere.
Tres ideas que hay que fijar antes de continuar:
- El bucle de eventos vive en el hilo principal, el mismo que ejecuta tu JavaScript. No son dos cosas en paralelo. Mientras tu callback se ejecuta, el bucle está parado esperando a que le devuelvas el control. Esta es la razón mecánica de por qué bloquear el hilo principal lo bloquea todo.
- Cada fase tiene su propia cola de callbacks. No hay una única "cola de eventos"; hay varias, y la fase actual determina cuál se vacía.
- Dentro de una fase, los callbacks se ejecutan hasta agotar su cola (con un límite de seguridad en algunas fases) y solo entonces se pasa a la siguiente.
- Las seis fases, en orden
flowchart TD
START(["Arranque:<br/>se ejecuta tu script completo"]) --> T
T["<b>1. timers</b><br/>callbacks de setTimeout<br/>y setInterval vencidos"]
P["<b>2. pending callbacks</b><br/>callbacks de E/S aplazados<br/>de la iteracion anterior"]
I["<b>3. idle / prepare</b><br/>uso interno de libuv"]
POLL["<b>4. poll</b><br/>recoge eventos de E/S nueva<br/>y ejecuta sus callbacks.<br/><i>Aqui es donde se espera</i>"]
CH["<b>5. check</b><br/>callbacks de setImmediate"]
CL["<b>6. close callbacks</b><br/>eventos 'close' de sockets,<br/>servidores y streams"]
FIN{"Quedan<br/>referencias?"}
T --> P --> I --> POLL --> CH --> CL --> FIN
FIN -->|"Si"| T
FIN -->|"No"| END(["El proceso termina"])
Entre cada par de fases —y también entre cada callback individual, en Node 11 y posteriores— se vacían dos colas especiales que no forman parte del diagrama porque no son fases del bucle: la de process.nextTick y la de las microtareas de promesas. Las veremos en el apartado 10, y son la fuente número uno de confusión.
Aquí tienes la vista de conjunto antes de entrar en el detalle:
| # | Fase | Qué callbacks ejecuta | ¿La ves a menudo? |
|---|---|---|---|
| 1 | timers | setTimeout y setInterval cuyo plazo ya venció |
Constantemente |
| 2 | pending callbacks | Callbacks de operaciones del sistema aplazados (p. ej. ECONNREFUSED de TCP) |
Rara vez de forma consciente |
| 3 | idle, prepare | Uso interno de libuv | Nunca desde JavaScript |
| 4 | poll | Callbacks de E/S: fichero leído, petición HTTP recibida, socket con datos | Es donde vive un servidor |
| 5 | check | setImmediate |
Cuando lo pides explícitamente |
| 6 | close callbacks | socket.on('close'), servidor.on('close') |
Al cerrar recursos |
- Fase 1: timers
En esta fase el bucle mira su montón de temporizadores y ejecuta los callbacks de todos aquellos cuyo plazo ya ha vencido.
El matiz esencial: setTimeout(fn, 100) no significa "ejecuta fn dentro de exactamente 100 ms". Significa "no ejecutes fn antes de que pasen 100 ms". El umbral es un mínimo, no una cita.
// src/laboratorio/timers-precision.js
// Un temporizador es un minimo, no una promesa de puntualidad.
const inicio = Date.now();
setTimeout(() => {
console.log(`Temporizador de 100 ms disparado a los ${Date.now() - inicio} ms`);
}, 100);
// Un bloqueo sincrono de 300 ms justo despues de programarlo.
const limite = Date.now() + 300;
while (Date.now() < limite) {
// Trabajo sincrono: el bucle de eventos ni siquiera ha empezado a girar.
}
console.log(`Bloqueo sincrono terminado a los ${Date.now() - inicio} ms`);El temporizador estaba vencido desde los 100 ms, pero el bucle no pudo atenderlo hasta que el hilo principal quedó libre. Un temporizador nunca se dispara antes de su plazo, pero puede dispararse muchísimo después. En un servidor saturado, esa diferencia es la señal de que algo va mal.
Nota sobre setInterval: no garantiza una cadencia exacta. Si el callback tarda más que el intervalo, Node no acumula ejecuciones pendientes: descarta las que no cabían y programa la siguiente. Un setInterval(fn, 100) cuyo fn tarda 250 ms acaba corriendo cada 250 ms, no cada 100.
- Fase 2: pending callbacks
Esta fase ejecuta callbacks de ciertas operaciones del sistema que quedaron aplazados de la iteración anterior. El caso típico es un error de TCP: cuando intentas conectar a un puerto cerrado, algunos sistemas notifican el ECONNREFUSED de forma que libuv lo encola aquí en lugar de en poll.
Es la fase que menos vas a tocar directamente. Debes saber que existe —porque explica algún orden de ejecución aparentemente raro con errores de red— pero no vas a programar nada en ella. También se la conoce en la documentación antigua como pending i/o callbacks.
- Fase 3: idle y prepare
Uso interno de libuv, sin API pública desde JavaScript. Node las utiliza para su propia preparación antes de entrar en poll. Aparecen en todos los diagramas por completitud y para que, cuando leas la documentación oficial, no te preguntes qué te has perdido. Puedes ignorarlas.
- Fase 4: poll, el corazón del corazón
Si hay una fase que debes entender de verdad, es esta. Un servidor Node pasa el 99 % de su vida aquí.
La fase poll hace dos cosas:
- Calcular cuánto tiempo puede permitirse bloquear esperando eventos de E/S.
- Procesar los eventos de E/S que ya han llegado, ejecutando sus callbacks.
Y el algoritmo de decisión, que es lo interesante, es este:
flowchart TD
A["Entramos en la fase poll"] --> B{"Hay callbacks de E/S<br/>en la cola?"}
B -->|"Si"| C["Ejecutarlos hasta agotar la cola<br/>(o alcanzar el limite del sistema)"]
C --> Z["Pasar a la fase check"]
B -->|"No"| D{"Hay setImmediate<br/>programados?"}
D -->|"Si"| Z2["Terminar poll YA<br/>y pasar a check"]
D -->|"No"| E{"Hay temporizadores<br/>proximos a vencer?"}
E -->|"Si"| F["BLOQUEAR aqui esperando E/S,<br/>como maximo hasta que venza<br/>el temporizador mas proximo"]
E -->|"No"| G["BLOQUEAR aqui esperando E/S<br/>indefinidamente"]
F --> Z
G --> Z
Cuatro consecuencias prácticas de este algoritmo:
- "Bloquear" aquí es lo que quieres, no un problema. Cuando tu servidor no tiene nada que hacer, se queda dormido en la llamada a
epoll_waitdel sistema operativo, sin consumir CPU, hasta que llega un paquete de red. Un servidor Node en reposo gasta 0 % de procesador. Es el mismo camarero de la primera lección: no espera de pie mirando la cocina, se sienta hasta que suena la campana. setImmediatetiene prioridad sobre esperar. Si hay unsetImmediatependiente, poll no se duerme: corta y pasa a check. Esto es lo que hace quesetImmediatesea fiable dentro de callbacks de E/S.- Los temporizadores acotan la espera. Si hay un
setTimeoutque vence en 50 ms, poll dormirá como mucho 50 ms, para poder volver a la fase timers a tiempo. - La cola de poll tiene un límite. Node no procesa infinitos eventos de E/S seguidos: existe un tope para no dejar sin atender al resto de fases. Lo sobrante se atiende en la siguiente iteración.
- Fase 5: check y
setImmediate
setImmediateLa fase check existe por un único motivo: dar a setImmediate un lugar garantizado justo después de la fase poll.
El nombre de setImmediate es engañoso: no significa "ejecútalo ya", significa "ejecútalo en cuanto termine de procesar la E/S de esta iteración". Es decir: en la fase check de la iteración actual.
// src/laboratorio/check.js
const fs = require('node:fs');
fs.readFile(__filename, () => {
// Estamos en la fase POLL (este es un callback de E/S).
console.log('1. Callback de lectura de fichero (fase poll)');
setImmediate(() => {
console.log('2. setImmediate (fase check, misma iteracion)');
});
setTimeout(() => {
console.log('3. setTimeout 0 (fase timers, siguiente iteracion)');
}, 0);
});1. Callback de lectura de fichero (fase poll) 2. setImmediate (fase check, misma iteracion) 3. setTimeout 0 (fase timers, siguiente iteracion)
Este orden es siempre el mismo, sin excepción. Y ya sabes por qué: desde poll, la siguiente fase es check, mientras que para llegar a timers hay que completar la iteración entera y empezar otra.
- Fase 6: close callbacks
Cuando un recurso se cierra —un socket, un servidor, un stream—, su evento 'close' no se emite inmediatamente: se encola en esta última fase.
// src/laboratorio/cierre.js
const net = require('node:net');
const servidor = net.createServer();
servidor.listen(0, () => { // El puerto 0 significa "uno libre cualquiera"
console.log(`1. Servidor escuchando en el puerto ${servidor.address().port}`);
servidor.close(); // Pedimos el cierre...
console.log('2. close() ya ha vuelto, pero el evento aun no se ha emitido');
setImmediate(() => console.log('3. setImmediate (fase check)'));
});
servidor.on('close', () => {
console.log('4. Evento close (fase close callbacks)');
});1. Servidor escuchando en el puerto 43217 2. close() ya ha vuelto, pero el evento aun no se ha emitido 3. setImmediate (fase check) 4. Evento close (fase close callbacks)
Nota la lección de fondo, que se repetirá en toda tu carrera con Node: pedir que algo se cierre y que algo esté cerrado son dos momentos distintos. En el Módulo 11 esto será importante para apagar el servidor de Escena Viva de forma ordenada sin cortar peticiones en curso.
setTimeout frente a setImmediate
setTimeout frente a setImmediateAhora la pregunta clásica de las entrevistas, que tiene una respuesta con dos partes.
9.1 En el nivel superior del script: no determinista
// src/laboratorio/orden-no-determinista.js
setTimeout(() => console.log('setTimeout 0'), 0);
setImmediate(() => console.log('setImmediate'));Ejecuta esto cinco o seis veces seguidas:
Verás que el orden cambia entre ejecuciones:
La explicación: setTimeout(fn, 0) se convierte internamente en un plazo de 1 ms. Cuando el bucle arranca por primera vez y entra en la fase timers, comprueba si ha pasado ese milisegundo. Si el arranque del proceso (cargar módulos, inicializar V8) tardó más de 1 ms, el temporizador ya está vencido y se ejecuta primero; si tardó menos, no está vencido, el bucle sigue hasta check y gana setImmediate. Como ese tiempo de arranque depende de la carga de la máquina, el resultado es una carrera.
Regla práctica: nunca escribas código cuya corrección dependa del orden entre
setTimeoutysetImmediateen el nivel superior. Si te importa el orden, es que estás usando la herramienta equivocada.
9.2 Dentro de un callback de E/S: totalmente determinista
Como viste en el apartado 7, dentro de un callback de E/S estamos ya en la fase poll, y desde ahí check viene inmediatamente después mientras que timers queda a una iteración de distancia. setImmediate siempre gana. Sin excepciones, sin depender de la máquina.
| Contexto | Ganador | Por qué |
|---|---|---|
| Nivel superior del script | Impredecible | Depende de si ha pasado 1 ms desde el arranque |
Dentro de un callback de E/S (fs, net, http) |
setImmediate, siempre |
Estamos en poll; check es la fase siguiente |
Dentro de un setImmediate |
setTimeout, normalmente |
Ya hemos pasado check: el siguiente setImmediate va a la próxima iteración |
Dentro de un setTimeout |
Impredecible | Misma carrera del primer caso |
9.3 Cuándo usar cada uno
| Quiero… | Uso |
|---|---|
| Ceder el control ahora mismo para no bloquear, y seguir cuanto antes | setImmediate(fn) |
| Ejecutar algo dentro de N milisegundos | setTimeout(fn, N) |
| Trocear un trabajo largo en pedazos que no bloqueen | setImmediate entre pedazo y pedazo |
| Ejecutar algo antes de que el bucle continúe, cueste lo que cueste | process.nextTick (con cuidado, ver siguiente apartado) |
- Las dos colas que se cuelan:
process.nextTick y las microtareas
process.nextTick y las microtareasAquí está el 80 % de la confusión que sufre la gente con Node. Existen dos colas que no pertenecen a ninguna fase y que se vacían entre fases, y también entre cada callback individual:
| Cola | Se llena con | Prioridad |
|---|---|---|
| nextTick queue | process.nextTick(fn) |
La más alta. Se vacía primero |
| microtask queue | Promise.then/catch/finally, await, queueMicrotask |
La segunda. Se vacía después de la de nextTick |
Y la regla completa, en el orden exacto en que Node actúa:
Después de terminar cada callback, y antes de continuar con el siguiente o de cambiar de fase, Node:
- Vacía completamente la cola de
nextTick(incluidos losnextTickque se añadan mientras la vacía).- Vacía completamente la cola de microtareas (incluidas las que se añadan mientras la vacía).
Nota histórica importante: este comportamiento cambió en Node.js 11. Antes, las colas se vaciaban solo entre fases, no entre callbacks individuales. Si encuentras artículos antiguos con salidas distintas, es por eso. Todo lo de esta lección se refiere a Node 11 y posteriores, que es todo lo que se usa hoy.
Un ejemplo mínimo que lo demuestra:
// src/laboratorio/colas.js
console.log('1. sincrono');
setTimeout(() => console.log('5. setTimeout'), 0);
setImmediate(() => console.log('6. setImmediate'));
Promise.resolve().then(() => console.log('4. microtarea de promesa'));
process.nextTick(() => console.log('3. nextTick'));
console.log('2. sincrono fin');(Las dos últimas líneas pueden intercambiarse, por lo que ya sabes del apartado 9.1. Las cuatro primeras son inamovibles.)
Y la demostración de que las colas se vacían entre cada callback, no solo entre fases:
// src/laboratorio/colas-entre-callbacks.js
setTimeout(() => {
console.log('timer 1');
process.nextTick(() => console.log(' nextTick desde timer 1'));
}, 0);
setTimeout(() => {
console.log('timer 2');
process.nextTick(() => console.log(' nextTick desde timer 2'));
}, 0);Los dos temporizadores están en la misma fase. Si las colas se vaciaran solo al cambiar de fase, veríamos timer 1, timer 2 y luego los dos nextTick. Que no sea así confirma la regla: se vacían entre callback y callback.
¿Cuándo usar process.nextTick?
Casi nunca, y por eso mismo conviene saber los dos casos legítimos:
- Garantizar que un callback sea siempre asíncrono, incluso en la ruta de error inmediato. Lo veremos con detalle en la próxima lección, Callbacks y Programación Asíncrona, porque es su remedio canónico.
- Permitir que quien te llama registre oyentes antes de emitir un evento. Es lo que hace internamente Node en varios sitios: si un constructor emitiera un evento de forma síncrona, nadie habría tenido ocasión de suscribirse todavía.
// Patron 2: dar tiempo a registrar el oyente.
const EventEmitter = require('node:events');
class CargadorCatalogo extends EventEmitter {
constructor() {
super();
// MAL: nadie ha podido suscribirse todavia.
// this.emit('listo');
// BIEN: se emite despues de que el codigo que nos creo pueda hacer .on()
process.nextTick(() => this.emit('listo'));
}
}
const cargador = new CargadorCatalogo();
cargador.on('listo', () => console.log('Catalogo listo')); // Si funcionaEn caso de duda, usa setImmediate en lugar de nextTick. Es más seguro, porque respeta el ciclo del bucle en vez de saltárselo. La razón, en el apartado siguiente.
- Una traza completa que debes poder predecir
Este es el examen de la lección. Lee el programa entero, escribe en un papel el orden de salida que esperas y solo después ejecútalo.
// src/laboratorio/traza.js
// Reto: predecir el orden de las 8 letras antes de ejecutar.
console.log('A - sincrono');
setTimeout(() => {
console.log('B - primer setTimeout');
process.nextTick(() => console.log('C - nextTick dentro del primer setTimeout'));
Promise.resolve().then(() => console.log('D - promesa dentro del primer setTimeout'));
}, 0);
setTimeout(() => {
console.log('E - segundo setTimeout');
}, 0);
process.nextTick(() => console.log('F - nextTick del nivel superior'));
Promise.resolve().then(() => console.log('G - promesa del nivel superior'));
console.log('H - sincrono fin');La respuesta y su razonamiento:
A - sincrono H - sincrono fin F - nextTick del nivel superior G - promesa del nivel superior B - primer setTimeout C - nextTick dentro del primer setTimeout D - promesa dentro del primer setTimeout E - segundo setTimeout
Paso a paso:
| Paso | Qué ocurre | Salida |
|---|---|---|
| 1 | Se ejecuta el script de arriba abajo. Los setTimeout, el nextTick y el .then solo encolan, no ejecutan |
A, H |
| 2 | Termina el script. Antes de que el bucle empiece a girar, Node vacía la cola de nextTick |
F |
| 3 | Después, la cola de microtareas | G |
| 4 | Primera iteración, fase timers. Se ejecuta el primer callback vencido | B |
| 5 | Ese callback ha terminado: se vacía la cola de nextTick antes de seguir |
C |
| 6 | Y a continuación la de microtareas | D |
| 7 | Solo ahora se pasa al segundo callback de temporizador, que estaba en la misma fase | E |
| 8 | Sin más colas ni referencias, el bucle sale y el proceso termina | — |
Si acertaste el orden completo, entiendes el bucle de eventos. Si fallaste en C y D colocándolos detrás de E, tienes en la cabeza el modelo anterior a Node 11: las colas se vacían entre callbacks, no solo entre fases.
- La inanición del bucle: el peligro de
nextTick
nextTickLa cola de nextTick se vacía completamente, incluidos los nextTick que se encolen mientras se está vaciando. Eso abre una puerta a un fallo especialmente desagradable:
// src/laboratorio/inanicion.js
// AVISO: este programa NUNCA llega al setTimeout. Paralo con Ctrl+C.
let vueltas = 0;
function recursivoConNextTick() {
vueltas++;
if (vueltas % 1000000 === 0) {
console.log(`${vueltas} vueltas y el bucle sigue sin avanzar`);
}
process.nextTick(recursivoConNextTick); // Se vuelve a encolar a si mismo
}
setTimeout(() => {
console.log('Esto NO se imprime jamas');
}, 100);
recursivoConNextTick();El bucle de eventos nunca llega a la fase timers, porque antes de avanzar tiene que vaciar la cola de nextTick, y esa cola se rellena sola indefinidamente. El proceso consume 100 % de un núcleo, no atiende nada y no da ningún error. A esto se le llama inanición del bucle de eventos (event loop starvation), y es un fallo difícil de diagnosticar porque el proceso parece vivo.
Ahora la misma estructura con setImmediate:
// src/laboratorio/sin-inanicion.js
let vueltas = 0;
function recursivoConImmediate() {
vueltas++;
if (vueltas >= 1000000) return;
setImmediate(recursivoConImmediate);
}
setTimeout(() => {
console.log(`El setTimeout SI se ejecuta, tras ${vueltas} vueltas`);
}, 100);
recursivoConImmediate();La diferencia es que un setImmediate encolado durante la fase check se ejecuta en la siguiente iteración, no en la actual. Eso deja pasar el resto de fases y el temporizador llega a dispararse.
process.nextTick |
setImmediate |
|
|---|---|---|
| Cuándo se ejecuta | Antes de continuar, saltándose el bucle | En la fase check de una iteración del bucle |
| Recursión | Provoca inanición | Segura: cede el control cada vuelta |
| Prioridad | Máxima | Normal |
| Uso recomendado | Casos muy concretos (ver 10) | La opción por defecto para "más tarde, pero pronto" |
Esta es la regla que debe quedarte grabada: si necesitas aplazar trabajo, usa setImmediate. process.nextTick es una herramienta afilada para dos casos específicos.
setTimeout(fn, 0) no es inmediato
setTimeout(fn, 0) no es inmediatoYa lo has visto de pasada, pero merece su propio apartado porque genera errores reales.
Cuando escribes setTimeout(fn, 0), Node no programa 0 milisegundos. Internamente aplica esta corrección:
// Comportamiento interno, simplificado
if (!(plazo >= 1 && plazo <= 2147483647)) {
plazo = 1; // Cualquier valor fuera del rango se convierte en 1 ms
}Es decir: el mínimo real es 1 milisegundo. Y hay dos límites más que conviene conocer:
| Valor que escribes | Lo que ocurre realmente |
|---|---|
setTimeout(fn, 0) |
Plazo de 1 ms |
setTimeout(fn, -5) |
Plazo de 1 ms |
setTimeout(fn) (sin plazo) |
Plazo de 1 ms |
setTimeout(fn, 2147483647) |
~24,8 días. El máximo (entero de 32 bits con signo) |
setTimeout(fn, 2147483648) |
Desbordamiento: se convierte en 1 ms y Node avisa por consola. Se ejecuta ¡inmediatamente! |
Ese último caso es un error real y recurrente: alguien programa "un recordatorio dentro de 30 días" con setTimeout(fn, 30 * 24 * 60 * 60 * 1000) y el callback se dispara al instante, porque 2.592.000.000 supera el máximo. En Escena Viva, un recordatorio de "tu evento es mañana" jamás debe implementarse con un setTimeout largo: se implementa con una tarea programada externa o una cola de trabajos.
Y otra consecuencia práctica: el plazo mínimo real de 1 ms significa que 1000 setTimeout(fn, 0) encadenados tardan al menos un segundo. Para trocear trabajo sin esperar nada, setImmediate es la herramienta correcta y es mucho más rápida.
- Medir el retraso del bucle de eventos
Llegamos a la parte más aplicable de la lección. El retraso del bucle de eventos (event loop lag o delay) es la diferencia entre el momento en que un callback debería haberse ejecutado y el momento en que realmente se ejecutó.
Es la métrica de salud número uno de un servidor Node, por encima incluso de la CPU y la memoria, y la razón es directa: el retraso del bucle es, literalmente, el tiempo que tus usuarios están esperando por culpa de que el proceso está ocupado.
14.1 La medición manual
// src/utiles/medir-bucle.js
// Mide el retraso del bucle de eventos con un temporizador de referencia.
const INTERVALO_MS = 100;
function iniciarMedicion({ intervaloMs = INTERVALO_MS, umbralMs = 50 } = {}) {
// hrtime.bigint() da nanosegundos y no se ve afectado por cambios de hora.
let ultimaMarca = process.hrtime.bigint();
const temporizador = setInterval(() => {
const ahora = process.hrtime.bigint();
// Tiempo real transcurrido, en milisegundos.
const transcurridoMs = Number(ahora - ultimaMarca) / 1e6;
ultimaMarca = ahora;
// El retraso es el exceso sobre el intervalo previsto.
const retrasoMs = Math.max(0, transcurridoMs - intervaloMs);
if (retrasoMs > umbralMs) {
console.error(`[bucle] retraso de ${retrasoMs.toFixed(1)} ms`);
}
}, intervaloMs);
// Que la medicion no impida que el proceso termine.
temporizador.unref();
return () => clearInterval(temporizador);
}
module.exports = { iniciarMedicion };Este fichero ya es, técnicamente, un módulo con module.exports. En la lección Módulos CommonJS y require() formalizaremos qué significa exactamente esa línea; de momento acepta que expone la función para que otros ficheros la usen.
14.2 La medición precisa: perf_hooks
Node trae una herramienta específica y mucho más exacta, porque mide dentro de libuv en lugar de con un temporizador de JavaScript:
// src/laboratorio/monitor-bucle.js
const { monitorEventLoopDelay } = require('node:perf_hooks');
// resolution: cada cuantos ms toma una muestra.
const histograma = monitorEventLoopDelay({ resolution: 10 });
histograma.enable();
// Simulamos carga: un bloqueo de 200 ms en mitad del proceso.
setTimeout(() => {
const limite = Date.now() + 200;
while (Date.now() < limite) { /* bloqueo deliberado */ }
}, 300);
setTimeout(() => {
histograma.disable();
// Los valores vienen en nanosegundos: dividimos entre 1e6 para tener ms.
const aMs = (n) => (n / 1e6).toFixed(2);
console.log('Retraso del bucle de eventos:');
console.log(` minimo : ${aMs(histograma.min)} ms`);
console.log(` media : ${aMs(histograma.mean)} ms`);
console.log(` maximo : ${aMs(histograma.max)} ms`);
console.log(` percentil 50: ${aMs(histograma.percentile(50))} ms`);
console.log(` percentil 99: ${aMs(histograma.percentile(99))} ms`);
}, 1000);Retraso del bucle de eventos: minimo : 9.99 ms media : 15.42 ms maximo : 208.67 ms percentil 50: 10.21 ms percentil 99: 208.67 ms
Cómo leer esos números:
| Percentil 99 del retraso | Diagnóstico |
|---|---|
| < 10 ms | Sano. El proceso responde con holgura |
| 10 – 50 ms | Aceptable, pero hay algo que ocupa el hilo. Merece investigarse |
| 50 – 200 ms | Malo. Los usuarios lo notan en cada petición |
| > 200 ms | Crítico. Hay trabajo síncrono pesado. El servidor está funcionalmente caído a ratos |
Fíjate en algo importante del ejemplo: la media es de 15 ms, un número tranquilizador. El percentil 99 es de 208 ms, un desastre. Por eso en producción se vigilan percentiles y no medias: la media esconde exactamente los problemas que importan. En el Módulo 11 publicaremos esta métrica de forma continua para el servidor de Escena Viva.
- Escena Viva: por qué un cálculo síncrono degrada a todos
Aterricemos todo lo anterior en el proyecto. Imagina que en el panel del administrador de Escena Viva añadimos una vista con la ocupación global de la temporada: 3.000 sesiones a lo largo del año, con su porcentaje, su recaudación y su comparación con la temporada anterior.
La implementación ingenua es esta:
// src/laboratorio/ocupacion-sincrona.js
// Version INGENUA: recalcula todo dentro de la peticion, de forma sincrona.
function calcularOcupacionGlobal(sesiones) {
const porSala = new Map();
for (const sesion of sesiones) {
// Trabajo real por sesion: agregacion, formateo, comparativas...
const clave = sesion.sala;
const acumulado = porSala.get(clave) ?? { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
acumulado.aforo += sesion.aforo;
acumulado.vendidas += sesion.vendidas;
acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
porSala.set(clave, acumulado);
}
return porSala;
}Si esa función tarda 300 ms, y un administrador refresca su panel, ocurre lo siguiente durante esos 300 ms:
- Todas las peticiones de compra de entradas se detienen. No se ralentizan: se detienen.
- El retraso del bucle sube a 300 ms. El percentil 99 de todos los usuarios se dispara.
- Si el servidor atiende 200 peticiones por segundo, 60 personas quedan esperando por un panel que ni siquiera están mirando.
- Y si el administrador tiene el panel con auto-refresco cada 5 segundos, el servidor pasa el 6 % de su vida congelado, de forma permanente.
Lo grave es que esto no aparece en desarrollo. En tu portátil, con un solo usuario, 300 ms es una página que carga "un poco lenta". En producción, con tráfico, es una caída parcial que ningún registro de errores va a mostrarte, porque técnicamente nada ha fallado.
Las tres soluciones, en orden de sencillez:
| Solución | Qué hace | Dónde se ve |
|---|---|---|
Trocear con setImmediate |
Procesa 200 sesiones, cede el control, continúa. El retraso máximo baja a ~20 ms | Aquí mismo, apartado siguiente |
| Calcular fuera y cachear | El informe se calcula cada 5 minutos en segundo plano; la petición solo lee un valor ya hecho | Módulo 10 |
| Mover el cálculo a otro hilo o proceso | worker_threads o cluster: el cálculo ocurre fuera del hilo que atiende peticiones |
Módulo 10 |
Y así queda la versión troceada, que ya puedes escribir con lo que sabes hoy:
// src/laboratorio/ocupacion-troceada.js
// Version COOPERATIVA: cede el control al bucle cada TAMANO_LOTE sesiones.
const TAMANO_LOTE = 200;
function calcularOcupacionGlobalTroceado(sesiones, alTerminar) {
const porSala = new Map();
let indice = 0;
function procesarLote() {
const fin = Math.min(indice + TAMANO_LOTE, sesiones.length);
for (; indice < fin; indice++) {
const sesion = sesiones[indice];
const acumulado = porSala.get(sesion.sala) ??
{ aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
acumulado.aforo += sesion.aforo;
acumulado.vendidas += sesion.vendidas;
acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
porSala.set(sesion.sala, acumulado);
}
if (indice < sesiones.length) {
// Cedemos el control: el bucle atiende peticiones antes de seguir.
setImmediate(procesarLote);
} else {
alTerminar(porSala);
}
}
procesarLote();
}El tiempo total es prácticamente el mismo, incluso algo peor. Pero el tiempo de bloqueo continuo pasa de 300 ms a unos 20 ms, y entre lote y lote el servidor atiende compras con normalidad. Es un intercambio deliberado: sacrificas un poco de rendimiento del informe a cambio de no penalizar a nadie más.
Ese alTerminar(porSala) que ves al final es un callback, y su forma no es casual: es el patrón que estructura toda la asincronía clásica de Node y el tema de la lección siguiente.
Errores Comunes y Consejos
Error 1: creer que setTimeout(fn, 100) ejecuta a los 100 ms exactos.
Ejecuta como pronto a los 100 ms. Si el hilo está ocupado, se retrasa lo que haga falta.
Error 2: apoyarse en el orden entre setTimeout(fn, 0) y setImmediate en el nivel superior.
Es una carrera y cambia entre ejecuciones. Dentro de un callback de E/S sí es determinista.
Error 3: usar process.nextTick para "aplazar un poco" trabajo.
Se salta el bucle entero. En recursión provoca inanición y el proceso deja de responder sin dar ningún error. Usa setImmediate.
Error 4: programar plazos largos con setTimeout.
Por encima de 2.147.483.647 ms (~24,8 días) desborda y se ejecuta inmediatamente. Los recordatorios de Escena Viva irán a una cola de trabajos, no a un temporizador.
Error 5: vigilar la media del retraso del bucle en lugar del percentil 99. La media oculta los picos, y los picos son justo el problema.
Error 6: pensar que trocear con setImmediate resuelve el cálculo intensivo.
Lo hace tolerable, no lo resuelve. Si el cálculo es realmente pesado, sacarlo del hilo (Módulo 10) es la respuesta.
Error 7: creer que las promesas "van en otro hilo".
No. Una promesa se resuelve en la cola de microtareas, en el mismo hilo principal. await no paraleliza nada por sí solo.
Consejo 1: aprende la traza del apartado 11 de memoria. Si puedes predecir esas ocho letras, puedes predecir cualquier programa asíncrono en Node.
Consejo 2: cuando algo "ocurra en el orden equivocado", dibuja las fases. En el 95 % de los casos la respuesta es que estabas mezclando fases distintas del bucle.
Consejo 3: instala la medición del retraso desde el primer día. Es diez líneas de código y detecta problemas que ninguna otra métrica muestra.
Ejercicios
Ejercicio 1: predecir la traza
Sin ejecutar nada, escribe el orden exacto de la salida de este programa y justifica cada línea indicando en qué fase o cola se ejecuta.
const fs = require('node:fs');
console.log('1');
setTimeout(() => {
console.log('2');
setImmediate(() => console.log('3'));
process.nextTick(() => console.log('4'));
}, 0);
setImmediate(() => {
console.log('5');
process.nextTick(() => console.log('6'));
});
fs.readFile(__filename, () => {
console.log('7');
setTimeout(() => console.log('8'), 0);
setImmediate(() => console.log('9'));
});
Promise.resolve().then(() => console.log('10'));
process.nextTick(() => console.log('11'));
console.log('12');Indica también cuál es la única pareja de líneas cuyo orden relativo no está garantizado.
Ejercicio 2: detectar el bloqueo en Escena Viva
Escribe src/laboratorio/vigilante-bucle.js que:
- Use
monitorEventLoopDelaydenode:perf_hookspara medir el retraso. - Simule tráfico con un
setIntervalde 20 ms que cuente peticiones atendidas. - Ejecute, a los 500 ms, un cálculo síncrono sobre 3.000 sesiones de Escena Viva que tarde en torno a 400 ms.
- A los 2 segundos, imprima: peticiones atendidas frente a peticiones esperadas, y el histograma completo (mínimo, media, máximo, p50, p99).
- Devuelva
process.exitCode = 1si el percentil 99 supera los 50 ms, con un mensaje porstderrexplicando el diagnóstico.
Ejercicio 3: trocear el informe de ocupación
Partiendo de calcularOcupacionGlobalTroceado del apartado 15, escribe src/laboratorio/comparar-troceado.js que ejecute las dos versiones (síncrona y troceada) sobre las mismas 3.000 sesiones mientras un setInterval de 20 ms mide el tráfico atendido, y produzca una tabla comparativa con:
- Tiempo total de cálculo de cada versión.
- Bloqueo continuo máximo de cada versión.
- Peticiones atendidas durante cada versión.
Después, responde por escrito: ¿qué tamaño de lote elegirías para Escena Viva y por qué? ¿Qué pasa si lo pones a 1? ¿Y a 3000?
Soluciones
Solución 1
1 sincrono 12 sincrono 11 cola de nextTick, tras terminar el script 10 cola de microtareas, tras la de nextTick 2 fase timers, primera iteracion 4 cola de nextTick, al terminar el callback del timer 5 fase check, primera iteracion 6 cola de nextTick, al terminar el callback de setImmediate 3 fase check, SEGUNDA iteracion (se encolo estando ya en la primera) 7 fase poll (callback de fs.readFile) 9 fase check, misma iteracion que 7 8 fase timers, siguiente iteracion
Justificación de los tramos que suelen fallarse:
11antes que10: la cola denextTicktiene prioridad sobre la de microtareas de promesa. Siempre.4justo después de2: al terminar el callback del temporizador se vacían las colas antes de continuar. Es el comportamiento de Node 11+.3después de5y6: cuando se ejecuta el callback del temporizador (fase timers), programar unsetImmediatelo coloca en la fase check de esa misma iteración… pero elsetImmediatedel nivel superior (5) ya estaba encolado antes, así que va primero. Y3se encoló durante la iteración actual mientras ya estábamos camino de check: dependiendo del momento exacto puede entrar en la misma cola de check o en la siguiente. En la práctica,5siempre precede a3.7al final del grupo:fs.readFilees E/S real. Tarda más que todo lo anterior, así que su callback llega en una iteración posterior.9antes que8: dentro de un callback de E/S (fase poll),setImmediate(check, fase siguiente) siempre gana asetTimeout(timers, iteración siguiente). Este es el orden determinista del apartado 9.2.
La única pareja no garantizada en el conjunto es la posición relativa de 7 respecto al bloque 2/4/5/6/3: depende de lo que tarde el disco en devolver el fichero. En una máquina con el fichero en caché puede llegar antes de lo que muestra la traza. Todo lo demás es fijo.
Solución 2
// src/laboratorio/vigilante-bucle.js
// Mide el impacto de un calculo sincrono sobre el trafico de Escena Viva.
const { monitorEventLoopDelay } = require('node:perf_hooks');
const INTERVALO_TRAFICO_MS = 20;
const DURACION_MS = 2000;
const UMBRAL_P99_MS = 50;
const histograma = monitorEventLoopDelay({ resolution: 5 });
histograma.enable();
const inicio = Date.now();
let atendidas = 0;
const trafico = setInterval(() => {
atendidas++;
}, INTERVALO_TRAFICO_MS);
// Catalogo de temporada completa: 3000 sesiones.
const sesiones = [];
for (let i = 1; i <= 3000; i++) {
sesiones.push({
id: `ses-${String(Math.ceil(i / 3)).padStart(3, '0')}-${(i % 3) + 1}`,
sala: ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'][i % 3],
aforo: 420,
vendidas: i % 420,
precioCentimos: 2500
});
}
// Calculo sincrono deliberadamente caro (~400 ms).
function calcularOcupacionGlobal(sesiones) {
const porSala = new Map();
for (const sesion of sesiones) {
let ruido = 0;
for (let i = 0; i < 60000; i++) {
ruido += Math.sqrt(i); // Trabajo artificial.
}
const acumulado = porSala.get(sesion.sala) ??
{ aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
acumulado.aforo += sesion.aforo;
acumulado.vendidas += sesion.vendidas;
acumulado.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
porSala.set(sesion.sala, acumulado);
}
return porSala;
}
setTimeout(() => {
const t0 = Date.now();
calcularOcupacionGlobal(sesiones);
console.error(`[informe] calculo sincrono de ${Date.now() - t0} ms`);
}, 500);
setTimeout(() => {
clearInterval(trafico);
histograma.disable();
const aMs = (n) => (n / 1e6).toFixed(2);
const esperadas = Math.floor((Date.now() - inicio) / INTERVALO_TRAFICO_MS);
const p99 = histograma.percentile(99) / 1e6;
console.log('');
console.log('TRAFICO');
console.log(` Peticiones esperadas : ${esperadas}`);
console.log(` Peticiones atendidas : ${atendidas}`);
console.log(` Perdidas : ${esperadas - atendidas}`);
console.log('');
console.log('RETRASO DEL BUCLE DE EVENTOS');
console.log(` minimo : ${aMs(histograma.min)} ms`);
console.log(` media : ${aMs(histograma.mean)} ms`);
console.log(` maximo : ${aMs(histograma.max)} ms`);
console.log(` p50 : ${aMs(histograma.percentile(50))} ms`);
console.log(` p99 : ${aMs(histograma.percentile(99))} ms`);
if (p99 > UMBRAL_P99_MS) {
console.error('');
console.error(
`DIAGNOSTICO: el percentil 99 (${p99.toFixed(1)} ms) supera el umbral de ` +
`${UMBRAL_P99_MS} ms. Hay trabajo sincrono bloqueando el hilo principal. ` +
'Trocealo con setImmediate, cachea el resultado o muevelo a un worker.'
);
process.exitCode = 1;
}
}, DURACION_MS);Salida típica:
[informe] calculo sincrono de 412 ms TRAFICO Peticiones esperadas : 100 Peticiones atendidas : 80 Perdidas : 20 RETRASO DEL BUCLE DE EVENTOS minimo : 4.98 ms media : 12.31 ms maximo : 414.20 ms p50 : 5.12 ms p99 : 414.20 ms DIAGNOSTICO: el percentil 99 (414.2 ms) supera el umbral de 50 ms. ...
Fíjate una vez más en la disociación entre media (12 ms, aparentemente sana) y percentil 99 (414 ms, desastroso). Es exactamente lo que verías en un panel de producción mal configurado que solo muestre medias.
Solución 3
// src/laboratorio/comparar-troceado.js
// Compara el calculo sincrono con el troceado, midiendo el trafico atendido.
const TAMANO_LOTE = Number(process.argv[2]) || 200;
const INTERVALO_TRAFICO_MS = 20;
const sesiones = [];
for (let i = 1; i <= 3000; i++) {
sesiones.push({
sala: ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'][i % 3],
aforo: 420,
vendidas: i % 420,
precioCentimos: 2500
});
}
// Trabajo por sesion, identico en ambas versiones.
function acumular(porSala, sesion) {
let ruido = 0;
for (let i = 0; i < 60000; i++) {
ruido += Math.sqrt(i);
}
const a = porSala.get(sesion.sala) ?? { aforo: 0, vendidas: 0, recaudacionCentimos: 0 };
a.aforo += sesion.aforo;
a.vendidas += sesion.vendidas;
a.recaudacionCentimos += sesion.vendidas * sesion.precioCentimos;
porSala.set(sesion.sala, a);
}
function version1Sincrona() {
const porSala = new Map();
for (const sesion of sesiones) acumular(porSala, sesion);
return porSala;
}
function version2Troceada(alTerminar) {
const porSala = new Map();
let indice = 0;
let bloqueoMaximo = 0;
function procesarLote() {
const t0 = Date.now();
const fin = Math.min(indice + TAMANO_LOTE, sesiones.length);
for (; indice < fin; indice++) acumular(porSala, sesiones[indice]);
bloqueoMaximo = Math.max(bloqueoMaximo, Date.now() - t0);
if (indice < sesiones.length) setImmediate(procesarLote);
else alTerminar(porSala, bloqueoMaximo);
}
procesarLote();
}
// --- Medicion ---
let atendidas = 0;
const trafico = setInterval(() => { atendidas++; }, INTERVALO_TRAFICO_MS);
const resultados = [];
// Version 1
setTimeout(() => {
const base = atendidas;
const t0 = Date.now();
version1Sincrona();
const total = Date.now() - t0;
resultados.push({ version: 'sincrona', totalMs: total, bloqueoMaxMs: total, atendidas: atendidas - base });
// Version 2, en cuanto termina la primera.
const base2 = atendidas;
const t1 = Date.now();
version2Troceada((_, bloqueoMaximo) => {
resultados.push({
version: `troceada (lote ${TAMANO_LOTE})`,
totalMs: Date.now() - t1,
bloqueoMaxMs: bloqueoMaximo,
atendidas: atendidas - base2
});
clearInterval(trafico);
console.table(resultados);
});
}, 200);Resultado típico con lote de 200:
┌─────────┬──────────────────────┬─────────┬──────────────┬───────────┐ │ (index) │ version │ totalMs │ bloqueoMaxMs │ atendidas │ ├─────────┼──────────────────────┼─────────┼──────────────┼───────────┤ │ 0 │ 'sincrona' │ 408 │ 408 │ 0 │ │ 1 │ 'troceada (lote 200)'│ 431 │ 29 │ 21 │ └─────────┴──────────────────────┴─────────┴──────────────┴───────────┘
Análisis:
- El tiempo total empeora un 6 % (408 → 431 ms). Ese es el precio de ceder el control quince veces.
- El bloqueo continuo máximo cae de 408 ms a 29 ms: una mejora de catorce veces en lo que de verdad afecta a los usuarios.
- Las peticiones atendidas pasan de 0 a 21. En la versión síncrona no se atendió absolutamente nada.
Respuesta razonada sobre el tamaño de lote:
- Lote de 1: el bloqueo máximo sería mínimo (~0,2 ms), pero se harían 3.000 vueltas del bucle. El tiempo total se dispararía por el coste de programar y despachar 3.000
setImmediate. Mucho trabajo administrativo para muy poca ganancia adicional. - Lote de 3000: es exactamente la versión síncrona, con el coste extra de la maquinaria de troceado. Lo peor de ambos mundos.
- Elección para Escena Viva: un lote calibrado para que cada trozo dure entre 5 y 20 ms. No se elige por número de elementos, sino por tiempo objetivo, porque el coste por sesión puede cambiar. Un enfoque más robusto es un lote adaptativo: medir cuánto tardó el lote anterior y ajustar el tamaño para acercarse a los 10 ms.
Y la conclusión honesta: trocear es una tirita, muy útil y aplicable hoy mismo, pero la solución definitiva para el panel de ocupación de Escena Viva es no calcularlo dentro de la petición — cachear el resultado o moverlo a un worker, ambas cosas en el Módulo 10.
Conclusión
Has recorrido el corazón de Node.js. Ahora sabes que el bucle de eventos es un while en C dentro de libuv que gira mientras queden referencias vivas, y que cada vuelta atraviesa seis fases en orden fijo: timers para los setTimeout vencidos, pending callbacks para callbacks de sistema aplazados, idle/prepare de uso interno, poll —donde un servidor pasa su vida, esperando E/S sin gastar CPU—, check para setImmediate, y close callbacks para los eventos de cierre.
Has entendido las decisiones de la fase poll: bloquea esperando eventos, pero corta inmediatamente si hay un setImmediate pendiente y acota su espera si hay un temporizador próximo. De ahí sale la respuesta completa a la pregunta clásica: setTimeout(fn, 0) contra setImmediate es una carrera impredecible en el nivel superior, pero dentro de un callback de E/S setImmediate gana siempre.
Has visto las dos colas que no son fases y que se cuelan entre cada callback desde Node 11: la de process.nextTick, con la máxima prioridad, y la de las microtareas de promesas justo detrás. Y has ejecutado la traza de las ocho letras hasta poder predecirla. Sabes también que nextTick recursivo provoca inanición del bucle —un proceso que consume un núcleo entero y no responde sin dar un solo error— y que la opción segura para aplazar trabajo es setImmediate. Y sabes que setTimeout(fn, 0) es en realidad 1 ms, y que por encima de 24,8 días desborda y se ejecuta al instante.
Sobre todo, tienes una herramienta de diagnóstico real: el retraso del bucle de eventos, medido con monitorEventLoopDelay, vigilado por percentil 99 y no por media. Y has comprobado en Escena Viva que un informe síncrono de 400 ms no ralentiza una petición: detiene el servidor entero, dejando sin atender a decenas de personas que solo querían comprar una entrada. Trocear con setImmediate bajó el bloqueo de 408 ms a 29 ms; la solución definitiva —cachear o mover a otro hilo— espera en el Módulo 10.
Todo este mecanismo existe para servir a una forma concreta de escribir código: entregar a Node una función que se llamará cuando el trabajo esté listo. Esa función tiene un nombre y una forma canónica en Node, con el error siempre en primer lugar, y unas reglas que si se rompen producen errores desconcertantes. En la siguiente lección, Callbacks y Programación Asíncrona, escribirás tus propias funciones asíncronas para Escena Viva, descubrirás por qué try/catch deja de funcionar, y construirás —a propósito— la pirámide de la muerte que las promesas vendrán a derribar.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
