Tema
Qué es Node y el event loop
En esta lección entenderás qué es Node.js, por qué es tan rápido para aplicaciones web y cómo funciona el event loop, la pieza que le permite atender miles de peticiones con un solo hilo. Entender esto te evitará los errores de rendimiento más comunes.
Concepto
JavaScript fuera del navegador
Durante años JavaScript sólo vivía en el navegador. En 2009, Ryan Dahl tomó V8 (el motor de JavaScript de Chrome) y lo envolvió con una librería en C llamada libuv, que da acceso al sistema operativo: archivos, red, temporizadores, procesos. El resultado fue Node.js.
| En el navegador | En Node.js |
|---|---|
window, document, DOM | No existen |
fetch, setTimeout, console | También existen |
| No puede leer tus archivos | fs lee y escribe archivos |
| No puede abrir puertos | http crea servidores |
globalThis es window | globalThis es el objeto global de Node |
La analogía del mesero
Imagina un restaurante:
- Modelo tradicional (un hilo por petición): cada cliente tiene un mesero que toma la orden, va a la cocina y se queda esperando hasta que el platillo esté listo. Con 1000 clientes necesitas 1000 meseros.
- Modelo Node (event loop): un solo mesero toma la orden, la deja en la cocina y sigue atendiendo otras mesas. Cuando la cocina avisa "¡orden lista!", el mesero la lleva.
La cocina es el sistema operativo y libuv (que sí usan varios hilos internos para disco y red). El mesero es tu código JavaScript, que corre en un solo hilo.
Por eso Node brilla en tareas de entrada/salida (I/O): consultar bases de datos, leer archivos, llamar a otras APIs. Y sufre con tareas intensivas de CPU (procesar video, cálculos enormes) porque mientras el mesero calcula, nadie más es atendido.
I/O bloqueante vs no bloqueante
js
import fs from 'node:fs'
// ❌ Bloqueante: el hilo se detiene hasta terminar de leer
const datos = fs.readFileSync('archivo-grande.txt', 'utf8')
console.log('Esto espera a que termine la lectura')
// ✅ No bloqueante: Node sigue trabajando y te avisa al terminar
fs.readFile('archivo-grande.txt', 'utf8', (err, contenido) => {
console.log('Lectura terminada')
})
console.log('Esto se imprime ANTES de que termine la lectura')El event loop paso a paso
El event loop es un ciclo infinito que revisa colas de tareas pendientes. Simplificado, cada "vuelta" tiene estas fases:
txt
┌───────────────────────────┐
┌─>│ timers │ ← callbacks de setTimeout / setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← algunos errores del sistema
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← espera y ejecuta I/O (red, disco)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← callbacks de setImmediate
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ ← 'close' de sockets
└───────────────────────────┘Además, entre cada callback se vacían dos colas de alta prioridad:
process.nextTick()(la más prioritaria).- Microtareas: promesas resueltas (
.then,await),queueMicrotask.
Ejemplo: adivina el orden
js
// orden.js
console.log('1. inicio (código síncrono)')
setTimeout(() => console.log('5. setTimeout (fase timers)'), 0)
setImmediate(() => console.log('6. setImmediate (fase check)'))
Promise.resolve().then(() => console.log('4. promesa (microtarea)'))
process.nextTick(() => console.log('3. nextTick'))
console.log('2. fin (código síncrono)')Ejecuta node orden.js:
txt
1. inicio (código síncrono)
2. fin (código síncrono)
3. nextTick
4. promesa (microtarea)
5. setTimeout (fase timers)
6. setImmediate (fase check)Nota
Desde el módulo principal, el orden entre setTimeout(..., 0) y setImmediate puede variar según el sistema. Dentro de un callback de I/O, setImmediate siempre va primero.
Además, este archivo no usa import, así que Node lo ejecuta como CommonJS. Si lo ejecutas como módulo ESM (.mjs), las promesas pueden imprimirse antes que nextTick, porque el propio módulo se evalúa dentro de una microtarea. No dependas de ese detalle en código real.
La regla práctica:
- Primero todo el código síncrono.
- Luego
nextTicky promesas. - Luego las fases del event loop (timers, I/O, immediate...).
Casos de uso reales
Bloquear el event loop (y cómo evitarlo)
js
// servidor-bloqueado.js
import http from 'node:http'
function calculoPesado() {
// Simula 5 segundos de CPU al 100%
const fin = Date.now() + 5000
while (Date.now() < fin) {}
return 'listo'
}
http.createServer((req, res) => {
if (req.url === '/lento') {
res.end(calculoPesado()) // ❌ Congela TODO el servidor 5 s
} else {
res.end('rápido')
}
}).listen(3000, () => console.log('http://localhost:3000'))Abre /lento en una pestaña y / en otra: la segunda también espera 5 segundos. Todo el servidor quedó congelado.
Soluciones profesionales:
- Dividir el trabajo en partes con
setImmediate. - Usar Worker Threads (
node:worker_threads) para cálculos pesados. - Mandar el trabajo a una cola (BullMQ + Redis) y procesarlo en otro proceso.
js
// worker-ejemplo.js — mover el cálculo a otro hilo
import { Worker, isMainThread, parentPort } from 'node:worker_threads'
if (isMainThread) {
const worker = new Worker(new URL(import.meta.url))
worker.on('message', (msg) => console.log('Resultado del worker:', msg))
console.log('El hilo principal sigue libre')
} else {
let suma = 0
for (let i = 0; i < 1e9; i++) suma += i
parentPort.postMessage(suma)
}¿Cuándo elegir Node?
| Ideal para | No tan ideal para |
|---|---|
| APIs REST y GraphQL | Procesamiento de video/imágenes masivo |
| Chats y apps en tiempo real (WebSockets) | Machine learning pesado |
| Microservicios | Cálculos científicos |
| Herramientas CLI y scripts | |
| SSR de React/Vue (Next, Nuxt) |
Buena práctica
Usa siempre las versiones asíncronas (fs/promises, drivers con promesas) dentro de un servidor. Las versiones Sync sólo están bien en scripts o al arrancar la app.
Error común
Pensar que "un solo hilo" significa que Node es lento. Es al revés: para I/O es muy eficiente. Lo que lo vuelve lento es bloquear ese hilo con código síncrono pesado.
Ejercicios
- Sin ejecutarlo, escribe el orden en que se imprime este código. Luego compruébalo.
js
console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
process.nextTick(() => console.log('D'))
;(async () => {
console.log('E')
await null
console.log('F')
})()
console.log('G')Ver solución
txt
A
E
G
D
C
F
BA, E y G son síncronos (el cuerpo de una función async corre síncrono hasta el primer await). Luego nextTick (D), después las microtareas en orden de registro (C y luego F, que se reanuda tras el await). Al final el timer (B).
- Modifica
servidor-bloqueado.jspara que/lentouse un Worker Thread y no bloquee la ruta/.
Ver solución
js
// servidor-worker.js
import http from 'node:http'
import { Worker, isMainThread, parentPort } from 'node:worker_threads'
if (isMainThread) {
http.createServer((req, res) => {
if (req.url === '/lento') {
const worker = new Worker(new URL(import.meta.url))
worker.once('message', (resultado) => res.end(`Resultado: ${resultado}`))
worker.once('error', () => { res.statusCode = 500; res.end('Error') })
} else {
res.end('rápido')
}
}).listen(3000, () => console.log('http://localhost:3000'))
} else {
const fin = Date.now() + 5000
while (Date.now() < fin) {}
parentPort.postMessage('listo')
}Recuerda guardar el archivo con extensión .mjs o tener "type": "module" en package.json (lo veremos en la lección de módulos).
Resumen
- Node.js = motor V8 + libuv: JavaScript con acceso al sistema operativo.
- Tu código corre en un solo hilo; el I/O se delega y no bloquea.
- El event loop procesa: código síncrono →
nextTick→ promesas → timers → I/O →setImmediate. - Node es excelente para APIs y tiempo real; evita bloquearlo con CPU intensiva (usa Worker Threads o colas).