Node.js Event Loop Explicado para Principiantes
Probablemente hayas oído que Node.js es de un solo hilo y utiliza un event loop para manejar miles de conexiones concurrentes. Pero ¿qué significa eso realmente? Si eres nuevo en Node.js, el event loop puede parecer una caja negra misteriosa. Este artículo lo desglosa en lenguaje sencillo para que puedas escribir código Node.js mejor y más rápido.
Por qué existe el event loop
Los servidores web tradicionales (como Apache con PHP) crean un nuevo hilo o proceso para cada solicitud. Eso funciona, pero no escala bien cuando tienes muchas conexiones concurrentes: cada hilo consume memoria y CPU. Node.js adopta un enfoque diferente: ejecuta tu JavaScript en un solo hilo y utiliza un event loop para manejar operaciones de I/O de forma asíncrona. Esto significa que tu código no espera a que termine una consulta a la base de datos o la lectura de un archivo; continúa ejecutando otras tareas y vuelve cuando el resultado está listo.
Los componentes principales
Antes de sumergirnos en el event loop, aclaremos los actores clave:
- Call Stack: Donde se ejecuta tu código JavaScript síncrono. Las funciones se apilan y se desapilan a medida que se ejecutan.
- Node APIs: APIs de C++ que manejan operaciones costosas como I/O de archivos, solicitudes de red y temporizadores. Se ejecutan en segundo plano (usando el grupo de hilos de libuv).
- Callback Queue: Donde los callbacks de operaciones completadas de Node API esperan a ser ejecutados.
- Event Loop: El orquestador que verifica continuamente si el call stack está vacío y, si es así, mueve los callbacks de la cola al stack.
Cómo funciona el event loop: un ejemplo simple
Considera este código:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
¿Cuál es la salida? Si adivinaste Start, End, Timeout, estás en lo correcto. Aunque el timeout es de 0 milisegundos, el callback no se ejecuta inmediatamente. Esto es lo que sucede:
console.log('Start')se apila en el call stack, se ejecuta y se desapila.- Se llama a
setTimeout. Node.js registra el temporizador y lo establece para que expire después de 0 ms. El callback se almacena y la función retorna. console.log('End')se apila, se ejecuta y se desapila.- Ahora el call stack está vacío. El event loop verifica la fase de temporizadores y ve que el temporizador ha expirado. Mueve el callback al call stack, que imprime
Timeout.
Esto demuestra la naturaleza no bloqueante: el temporizador no bloquea el resto del código.
Las fases del event loop
El event loop procesa callbacks en varias fases, cada una con un propósito específico. Comprender estas fases te ayuda a predecir el orden de ejecución.
| Fase | Descripción |
|---|---|
| Timers | Ejecuta callbacks programados por setTimeout() y setInterval(). |
| Pending Callbacks | Ejecuta callbacks de I/O diferidos a la siguiente iteración del loop. |
| Idle, Prepare | Usado internamente por Node.js. |
| Poll | Recupera nuevos eventos de I/O; ejecuta callbacks relacionados con I/O (casi todos excepto timers, setImmediate y close callbacks). |
| Check | Ejecuta callbacks programados por setImmediate(). |
| Close Callbacks | Ejecuta callbacks de cierre, por ejemplo, socket.on('close', ...). |
Entre cada fase, Node.js verifica microtasks: callbacks de process.nextTick() y Promesas. Estos tienen mayor prioridad y se ejecutan inmediatamente después de que la operación actual finaliza, antes de pasar a la siguiente fase.
Microtasks: nextTick y Promesas
Las microtasks no forman parte de las fases del event loop; se procesan después de cada fase y después de cada callback. Esto hace que se ejecuten antes que los timers y los callbacks de I/O. Por ejemplo:
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('Sync');
Salida: Sync, nextTick, Promise, Timeout. El código síncrono se ejecuta primero, luego las microtasks (nextTick antes que Promesas), luego el temporizador.
Advertencia: Las llamadas recursivas a process.nextTick() pueden dejar sin recursos al event loop, impidiendo que ocurra I/O. Usa setImmediate() para operaciones recursivas.
setImmediate vs setTimeout
setImmediate() está diseñado para ejecutar un callback inmediatamente después de que se completa la fase de poll actual. En contraste, setTimeout() con 0 ms espera a la siguiente fase de temporizadores. El orden entre ellos puede variar según el contexto, pero dentro de un callback de I/O, setImmediate() siempre se ejecuta primero.
Por qué esto importa para tu código
Comprender el event loop te ayuda a evitar errores comunes:
- No bloquees el event loop: Operaciones síncronas como
fs.readFileSynco cálculos pesados bloquean todo el loop, haciendo que tu aplicación no responda. Usa versiones asíncronas o delega a worker threads. - Usa microtasks con sabiduría: Se ejecutan antes de I/O, por lo que un procesamiento pesado de microtasks puede retrasar los callbacks de I/O.
- Comprende el orden de los callbacks: Al mezclar timers, Promesas e I/O, conoce las fases para predecir la salida.
Ejemplo práctico: lectura de archivos no bloqueante
Así es como el event loop permite I/O no bloqueante:
const fs = require('fs');
console.log('Before read');
fs.readFile('large-file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('File read complete');
});
console.log('After read');
Salida: Before read, After read, luego File read complete. La lectura del archivo ocurre en segundo plano y el callback se encola cuando termina. Mientras tanto, otro código puede ejecutarse.
Preguntas frecuentes
¿Node.js es realmente de un solo hilo?
Node.js ejecuta tu JavaScript en un solo hilo, pero utiliza múltiples hilos en el grupo de hilos de libuv para I/O de archivos, DNS y otras operaciones. Así que no es completamente de un solo hilo bajo el capó.
¿Cuál es la diferencia entre el call stack y el event loop?
El call stack ejecuta código síncrono. El event loop gestiona callbacks asíncronos, moviéndolos de las colas al call stack cuando está vacío.
¿Puedo crear múltiples event loops?
No, cada proceso de Node.js tiene un event loop. Sin embargo, puedes usar worker threads para ejecutar hilos de JavaScript separados, cada uno con su propio event loop.
Si estás trabajando con registros de servidor para depurar problemas de rendimiento, prueba nuestro Analizador de Registros de Nginx para analizar y visualizar rápidamente los datos de los registros.