Skip to content

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 navegadorEn Node.js
window, document, DOMNo existen
fetch, setTimeout, consoleTambién existen
No puede leer tus archivosfs lee y escribe archivos
No puede abrir puertoshttp crea servidores
globalThis es windowglobalThis 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:

  1. process.nextTick() (la más prioritaria).
  2. 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:

  1. Primero todo el código síncrono.
  2. Luego nextTick y promesas.
  3. 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 paraNo tan ideal para
APIs REST y GraphQLProcesamiento de video/imágenes masivo
Chats y apps en tiempo real (WebSockets)Machine learning pesado
MicroserviciosCá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 ​

  1. 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
B

A, 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).

  1. Modifica servidor-bloqueado.js para que /lento use 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).

Academia Web — aprende, copia y construye.