Tabla de Contenidos
- ¿Qué es Node.js?
- Arquitectura del Runtime
- El Event Loop
- Call Stack, Callback Queue y Microtask Queue
- Blocking vs Non-blocking
- Manejo de Asincronía
- libuv y el Thread Pool
- Cuestionario de Entrevista
¿Qué es Node.js?
Node.js es un runtime de JavaScript construido sobre el motor V8 de Chrome. Esto significa que permite ejecutar código JavaScript fuera del navegador, específicamente en el servidor.
Comparación con Python y Ruby
// Node.js
console.log("Hello from Node.js");
# Python
print("Hello from Python")
# Ruby
puts "Hello from Ruby"
Diferencia fundamental:
- Python/Ruby: Lenguajes interpretados con runtimes single-threaded por defecto (aunque Python tiene threading/multiprocessing y Ruby tiene threads)
- Node.js: Runtime single-threaded con arquitectura asíncrona no bloqueante basada en eventos
Analogía: El Restaurante
Imagina tres restaurantes diferentes:
Restaurante Python/Ruby (Modelo Síncrono):
- Un mesero atiende una mesa a la vez
- Toma el pedido, espera en la cocina hasta que esté listo, sirve, cobra
- Solo entonces atiende la siguiente mesa
- Para atender más clientes: contratas más meseros (threads/processes)
Restaurante Node.js (Modelo Asíncrono):
- Un mesero (single thread) atiende muchas mesas
- Toma pedidos de múltiples mesas
- Delega a la cocina (operaciones I/O)
- Mientras la cocina trabaja, sigue tomando más pedidos
- Cuando un plato está listo (evento), lo entrega
- Puede atender cientos de mesas sin contratar más meseros
Arquitectura del Runtime
Node.js está compuesto por varias capas:
┌─────────────────────────────────────┐
│ JavaScript (Tu Código) │
├─────────────────────────────────────┤
│ Node.js APIs (fs, http, etc) │
├─────────────────────────────────────┤
│ Node.js Bindings (C++) │
├─────────────────────────────────────┤
│ V8 Engine │ libuv │
│ (JavaScript VM) │ (Async I/O)│
└─────────────────────────────────────┘
Componentes Clave
1. V8 Engine:
- Motor de JavaScript de Google (escrito en C++)
- Compila JavaScript a código máquina
- Maneja la memoria (garbage collection)
- Ejecuta el código JavaScript
2. libuv:
- Biblioteca escrita en C
- Proporciona el Event Loop
- Maneja operaciones asíncronas (I/O)
- Implementa el Thread Pool
- Abstrae diferencias entre sistemas operativos
3. Node.js Bindings:
- Conecta JavaScript con C++
- Permite que tu código JS use funcionalidades del sistema operativo
4. Node.js APIs:
- fs (file system)
- http/https
- crypto
- stream
- etc.
El Event Loop
El Event Loop es el corazón de Node.js. Es el mecanismo que permite que Node.js realice operaciones no bloqueantes a pesar de que JavaScript es single-threaded.
¿Cómo Funciona?
El Event Loop es un ciclo infinito que ejecuta código, recopila y procesa eventos, y ejecuta subtareas en cola.
// Ejemplo conceptual del Event Loop
while (hayTareasPendientes) {
ejecutarFasesDelEventLoop();
if (hayEventosPendientes) {
procesarEventos();
}
}
Las 6 Fases del Event Loop
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
1. Timers (Temporizadores)
Ejecuta callbacks de setTimeout()
y setInterval()
cuyo tiempo ha expirado.
setTimeout(() => {
console.log('Timer ejecutado después de 1000ms');
}, 1000);
Importante: El tiempo especificado es el tiempo mínimo de espera, no garantiza ejecución exacta.
2. Pending Callbacks
Ejecuta callbacks de I/O diferidos de la iteración anterior (errores de TCP, etc).
3. Idle, Prepare
Fase interna de Node.js. No escribes código que se ejecute aquí.
4. Poll (Encuesta)
La fase más importante. Aquí Node.js:
- Recupera nuevos eventos I/O
- Ejecuta callbacks relacionados con I/O
- Puede bloquear esperando eventos si no hay nada más que hacer
const fs = require('fs');
// Este callback se ejecuta en la fase Poll
fs.readFile('/archivo.txt', (err, data) => {
console.log('Archivo leído');
});
5. Check (Verificación)
Ejecuta callbacks de setImmediate()
.
setImmediate(() => {
console.log('Ejecutado en fase Check');
});
6. Close Callbacks
Ejecuta callbacks de cierre (ej: socket.on('close', ...)
).
const server = require('http').createServer();
server.on('close', () => {
console.log('Servidor cerrado');
});
Ejemplo Completo del Event Loop
const fs = require('fs');
console.log('1. Inicio del programa');
// Fase: Timers
setTimeout(() => {
console.log('2. setTimeout 0ms');
}, 0);
// Fase: Check
setImmediate(() => {
console.log('3. setImmediate');
});
// Fase: Poll
fs.readFile(__filename, () => {
console.log('4. readFile callback');
// Dentro de un callback de I/O:
setTimeout(() => {
console.log('5. setTimeout dentro de readFile');
}, 0);
setImmediate(() => {
console.log('6. setImmediate dentro de readFile');
});
});
console.log('7. Fin del programa');
// Salida:
// 1. Inicio del programa
// 7. Fin del programa
// 2. setTimeout 0ms
// 3. setImmediate
// 4. readFile callback
// 6. setImmediate dentro de readFile
// 5. setTimeout dentro de readFile
¿Por qué este orden?
- El código síncrono se ejecuta primero (1, 7)
- En la siguiente iteración del Event Loop:
-
setTimeout(0)
se ejecuta en fase Timers (2) -
setImmediate
se ejecuta en fase Check (3)
-
-
readFile
completa en fase Poll (4) -
Dentro de un callback de I/O,
setImmediate
tiene prioridad sobresetTimeout
(6, 5)
Call Stack, Callback Queue y Microtask Queue
Call Stack (Pila de Llamadas)
El Call Stack es donde JavaScript rastrea la ejecución de funciones.
function tercera() {
console.log('Tercera función');
}
function segunda() {
tercera();
}
function primera() {
segunda();
}
primera();
// Call Stack durante la ejecución:
// │ tercera() │ <- Cima (se ejecuta ahora)
// │ segunda() │
// │ primera() │
// │ main() │ <- Base
// └───────────┘
Características:
- LIFO (Last In, First Out)
- Single-threaded: una sola pila
- Stack overflow cuando hay demasiadas llamadas anidadas
// Ejemplo de Stack Overflow
function recursiva() {
recursiva(); // Llamada infinita
}
recursiva(); // RangeError: Maximum call stack size exceeded
Callback Queue (Cola de Callbacks)
También llamada Task Queue o Macrotask Queue. Almacena callbacks listos para ejecutarse.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// Salida:
// 1
// 3
// 2
¿Por qué "2" se ejecuta al final?
-
console.log('1')
se ejecuta inmediatamente -
setTimeout
registra el callback en la Cola -
console.log('3')
se ejecuta inmediatamente - El Call Stack queda vacío
- El Event Loop toma el callback de la Cola y lo ejecuta
Microtask Queue (Cola de Microtareas)
Las microtareas tienen mayor prioridad que los callbacks normales.
Microtareas incluyen:
- Promises (
.then()
,.catch()
,.finally()
) -
process.nextTick()
(Node.js específico - mayor prioridad aún) queueMicrotask()
- MutationObserver (en navegadores)
console.log('1. Script inicio');
setTimeout(() => {
console.log('2. setTimeout (Macrotask)');
}, 0);
Promise.resolve()
.then(() => {
console.log('3. Promise 1 (Microtask)');
})
.then(() => {
console.log('4. Promise 2 (Microtask)');
});
console.log('5. Script fin');
// Salida:
// 1. Script inicio
// 5. Script fin
// 3. Promise 1 (Microtask)
// 4. Promise 2 (Microtask)
// 2. setTimeout (Macrotask)
El Orden Completo de Ejecución
console.log('1. Sincrónico 1');
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
process.nextTick(() => {
console.log('4. nextTick');
});
console.log('5. Sincrónico 2');
// Salida:
// 1. Sincrónico 1
// 5. Sincrónico 2
// 4. nextTick <- Mayor prioridad
// 3. Promise <- Microtask
// 2. setTimeout <- Macrotask
Regla de oro:
- Código sincrónico
process.nextTick()
- Microtasks (Promises)
- Macrotasks (setTimeout, setImmediate, I/O)
Diagrama Completo
┌─────────────────────────────────────────┐
│ JavaScript Call Stack │
│ (Ejecuta código sincrónico) │
└─────────────┬───────────────────────────┘
│
↓ (Stack vacío?)
┌─────────────────────────────────────────┐
│ process.nextTick Queue │
│ (Se ejecuta ANTES de microtasks) │
└─────────────┬───────────────────────────┘
│
↓ (Vacío?)
┌─────────────────────────────────────────┐
│ Microtask Queue │
│ (Promises, queueMicrotask) │
└─────────────┬───────────────────────────┘
│
↓ (Vacío?)
┌─────────────────────────────────────────┐
│ Macrotask Queue │
│ (setTimeout, setImmediate, I/O) │
└─────────────────────────────────────────┘
Blocking vs Non-blocking
Operaciones Bloqueantes (Blocking)
Bloquean el Event Loop hasta que completan.
const fs = require('fs');
console.log('Antes de leer');
// BLOQUEANTE - Bloquea el Event Loop
const data = fs.readFileSync('archivo.txt', 'utf8');
console.log(data);
console.log('Después de leer');
// Ejecución:
// 1. "Antes de leer"
// 2. [ESPERA hasta que el archivo se lea completamente]
// 3. Contenido del archivo
// 4. "Después de leer"
Problemas:
- El servidor no puede atender otras peticiones mientras espera
- Si 1000 usuarios hacen peticiones, se formaría una cola
Operaciones No Bloqueantes (Non-blocking)
Delegan el trabajo y continúan ejecutando.
const fs = require('fs');
console.log('Antes de leer');
// NO BLOQUEANTE - Continúa inmediatamente
fs.readFile('archivo.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
console.log('Después de leer');
// Ejecución:
// 1. "Antes de leer"
// 2. "Después de leer"
// 3. [Cuando el archivo esté listo] Contenido del archivo
Comparación con Python
Python (Blocking por defecto):
import time
print("Inicio")
# Bloquea el thread durante 2 segundos
time.sleep(2)
print("Fin")
# Salida:
# Inicio
# [espera 2 segundos]
# Fin
Python con asyncio (Non-blocking):
import asyncio
async def main():
print("Inicio")
# No bloquea, permite que otras tareas se ejecuten
await asyncio.sleep(2)
print("Fin")
asyncio.run(main())
Node.js (Non-blocking por defecto):
console.log("Inicio");
// No bloquea
setTimeout(() => {
console.log("Fin");
}, 2000);
console.log("Continúa");
// Salida:
// Inicio
// Continúa
// [después de 2 segundos]
// Fin
¿Cuándo Usar Cada Uno?
Operaciones Síncronas (Bloqueantes):
- ✅ Al inicio de la aplicación (cargar configuración)
- ✅ Scripts simples que no necesitan concurrencia
- ✅ Cuando el orden es crítico y no hay alternativa
- ❌ Durante el manejo de peticiones HTTP
Operaciones Asíncronas (No Bloqueantes):
- ✅ Operaciones I/O (archivos, bases de datos, red)
- ✅ Servidores web
- ✅ Cualquier operación que tome tiempo
- ✅ Cuando necesitas manejar múltiples operaciones concurrentes
Manejo de Asincronía
Node.js ha evolucionado en cómo maneja la asincronía:
1. Callbacks (El Patrón Original)
const fs = require('fs');
// Patrón Error-First Callback
fs.readFile('archivo1.txt', 'utf8', (err, data1) => {
if (err) {
console.error('Error leyendo archivo1:', err);
return;
}
console.log('Archivo 1:', data1);
// Callback anidado (Callback Hell)
fs.readFile('archivo2.txt', 'utf8', (err, data2) => {
if (err) {
console.error('Error leyendo archivo2:', err);
return;
}
console.log('Archivo 2:', data2);
// Más anidación...
fs.readFile('archivo3.txt', 'utf8', (err, data3) => {
if (err) {
console.error('Error leyendo archivo3:', err);
return;
}
console.log('Archivo 3:', data3);
});
});
});
Problemas:
- Callback Hell (Pirámide de la perdición)
- Difícil de leer y mantener
- Manejo de errores repetitivo
2. Promises (ES6)
Las Promises representan un valor que puede estar disponible ahora, en el futuro, o nunca.
const fs = require('fs').promises;
// Encadenamiento de Promises
fs.readFile('archivo1.txt', 'utf8')
.then(data1 => {
console.log('Archivo 1:', data1);
return fs.readFile('archivo2.txt', 'utf8');
})
.then(data2 => {
console.log('Archivo 2:', data2);
return fs.readFile('archivo3.txt', 'utf8');
})
.then(data3 => {
console.log('Archivo 3:', data3);
})
.catch(err => {
console.error('Error:', err);
});
Estados de una Promise
// PENDING (Pendiente)
const promise = new Promise((resolve, reject) => {
// Haciendo algo asíncrono...
});
// FULFILLED (Cumplida)
const fulfilled = Promise.resolve('Éxito');
// REJECTED (Rechazada)
const rejected = Promise.reject(new Error('Falló'));
Creando Promises
function leerArchivoPromise(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
reject(err); // Promise rechazada
} else {
resolve(data); // Promise cumplida
}
});
});
}
// Uso
leerArchivoPromise('archivo.txt')
.then(data => console.log(data))
.catch(err => console.error(err));
Promise.all() - Paralelismo
const fs = require('fs').promises;
// Leer múltiples archivos en paralelo
Promise.all([
fs.readFile('archivo1.txt', 'utf8'),
fs.readFile('archivo2.txt', 'utf8'),
fs.readFile('archivo3.txt', 'utf8')
])
.then(([data1, data2, data3]) => {
console.log('Todos leídos:', data1, data2, data3);
})
.catch(err => {
console.error('Al menos uno falló:', err);
});
Otros Métodos Útiles
// Promise.race() - La primera que complete
Promise.race([
fetch('https://api1.com/data'),
fetch('https://api2.com/data')
])
.then(response => console.log('La más rápida respondió'));
// Promise.allSettled() - Espera todas sin importar el resultado
Promise.allSettled([
Promise.resolve('Éxito 1'),
Promise.reject('Error 1'),
Promise.resolve('Éxito 2')
])
.then(results => {
// results = [
// { status: 'fulfilled', value: 'Éxito 1' },
// { status: 'rejected', reason: 'Error 1' },
// { status: 'fulfilled', value: 'Éxito 2' }
// ]
});
// Promise.any() - La primera que tenga éxito
Promise.any([
Promise.reject('Error 1'),
Promise.resolve('Éxito 1'),
Promise.resolve('Éxito 2')
])
.then(result => console.log(result)); // 'Éxito 1'
3. Async/Await (ES2017)
Azúcar sintáctico sobre Promises que hace el código asíncrono parecer síncrono.
const fs = require('fs').promises;
async function leerArchivos() {
try {
const data1 = await fs.readFile('archivo1.txt', 'utf8');
console.log('Archivo 1:', data1);
const data2 = await fs.readFile('archivo2.txt', 'utf8');
console.log('Archivo 2:', data2);
const data3 = await fs.readFile('archivo3.txt', 'utf8');
console.log('Archivo 3:', data3);
return 'Todos leídos exitosamente';
} catch (err) {
console.error('Error:', err);
throw err;
}
}
leerArchivos()
.then(resultado => console.log(resultado))
.catch(err => console.error('Error final:', err));
Reglas de Async/Await
-
async
siempre retorna una Promise
async function ejemplo() {
return 'Hola'; // Equivale a: return Promise.resolve('Hola')
}
ejemplo().then(console.log); // 'Hola'
-
await
solo funciona dentro de funcionesasync
// ❌ Error
function normal() {
await algo(); // SyntaxError
}
// ✅ Correcto
async function asincrona() {
await algo();
}
-
await
pausa la ejecución de la función (no del programa)
async function ejemplo() {
console.log('Antes');
await delay(1000);
console.log('Después'); // Se ejecuta 1 segundo después
}
console.log('Inicio');
ejemplo();
console.log('Fin');
// Salida:
// Inicio
// Antes
// Fin
// [después de 1 segundo]
// Después
Paralelismo con Async/Await
// ❌ SECUENCIAL (Lento - suma los tiempos)
async function secuencial() {
const data1 = await fs.readFile('archivo1.txt', 'utf8'); // 100ms
const data2 = await fs.readFile('archivo2.txt', 'utf8'); // 100ms
const data3 = await fs.readFile('archivo3.txt', 'utf8'); // 100ms
// Total: ~300ms
}
// ✅ PARALELO (Rápido - tiempo del más lento)
async function paralelo() {
const [data1, data2, data3] = await Promise.all([
fs.readFile('archivo1.txt', 'utf8'), // 100ms
fs.readFile('archivo2.txt', 'utf8'), // 100ms
fs.readFile('archivo3.txt', 'utf8') // 100ms
]);
// Total: ~100ms
}
Top-Level Await (ES2022)
// Archivo: main.mjs (requiere .mjs o "type": "module" en package.json)
// Antes tenías que envolver en una función async
// async function main() {
// const data = await fetch('...');
// }
// main();
// Ahora puedes usar await directamente
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
Comparación: Callbacks vs Promises vs Async/Await
// 1. CALLBACKS
function obtenerUsuarioCallback(id, callback) {
obtenerDeDatabaseCallback(id, (err, usuario) => {
if (err) return callback(err);
obtenerPostsCallback(usuario.id, (err, posts) => {
if (err) return callback(err);
obtenerComentariosCallback(posts[0].id, (err, comentarios) => {
if (err) return callback(err);
callback(null, { usuario, posts, comentarios });
});
});
});
}
// 2. PROMISES
function obtenerUsuarioPromise(id) {
return obtenerDeDatabasePromise(id)
.then(usuario => {
return obtenerPostsPromise(usuario.id)
.then(posts => {
return obtenerComentariosPromise(posts[0].id)
.then(comentarios => {
return { usuario, posts, comentarios };
});
});
});
}
// 3. ASYNC/AWAIT (Más legible)
async function obtenerUsuarioAsync(id) {
const usuario = await obtenerDeDatabase(id);
const posts = await obtenerPosts(usuario.id);
const comentarios = await obtenerComentarios(posts[0].id);
return { usuario, posts, comentarios };
}
Manejo de Errores Avanzado
async function manejoErrores() {
try {
const resultado = await operacionPeligrosa();
return resultado;
} catch (error) {
// Manejo específico por tipo de error
if (error.code === 'ENOENT') {
console.error('Archivo no encontrado');
} else if (error.code === 'EACCES') {
console.error('Permiso denegado');
} else {
console.error('Error desconocido:', error);
}
throw error; // Re-lanzar si es necesario
} finally {
// Se ejecuta siempre, haya error o no
console.log('Limpieza');
}
}
libuv y el Thread Pool
¿Qué es libuv?
libuv es una biblioteca multi-plataforma escrita en C que proporciona:
- El Event Loop
- Operaciones asíncronas de I/O
- Thread Pool para operaciones que no pueden ser asíncronas
- Timers
- Señales de proceso
- Y más...
El Thread Pool
Aunque Node.js es single-threaded para JavaScript, libuv utiliza un thread pool para operaciones que el sistema operativo no puede hacer de forma asíncrona.
Tamaño por defecto: 4 threads
// Ver el tamaño del thread pool
console.log(process.env.UV_THREADPOOL_SIZE); // undefined = 4 por defecto
// Cambiarlo (debe hacerse antes de cualquier operación que lo use)
process.env.UV_THREADPOOL_SIZE = 8;
Operaciones que Usan el Thread Pool
-
File System (fs) - Todas las operaciones excepto
fs.watch()
-
DNS lookups -
dns.lookup()
- Crypto - Operaciones criptográficas pesadas
- Zlib - Compresión/descompresión
const crypto = require('crypto');
const fs = require('fs');
// Estas operaciones usan el thread pool
crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', (err, key) => {
console.log('Crypto completado');
});
fs.readFile('archivo.txt', (err, data) => {
console.log('Archivo leído');
});
Operaciones Verdaderamente Asíncronas (No usan Thread Pool)
Estas usan las capacidades asíncronas del sistema operativo:
- Networking (TCP/UDP)
- HTTP requests
- Timers (setTimeout, setInterval)
const http = require('http');
// No usa thread pool - usa epoll/kqueue del sistema operativo
http.get('http://example.com', (res) => {
console.log('Respuesta recibida');
});
Ejemplo: Impacto del Thread Pool
const crypto = require('crypto');
const inicio = Date.now();
// Ejecutar 8 operaciones pesadas
for (let i = 0; i < 8; i++) {
crypto.pbkdf2('password', 'salt', 100000, 512, 'sha512', () => {
console.log(`${i + 1}: ${Date.now() - inicio}ms`);
});
}
// Con UV_THREADPOOL_SIZE=4 (default):
// 1: 523ms
// 2: 524ms
// 3: 525ms
// 4: 526ms
// 5: 1045ms <- Segunda tanda
// 6: 1046ms
// 7: 1047ms
// 8: 1048ms
// Con UV_THREADPOOL_SIZE=8:
// 1-8: todos ~520ms (todas en paralelo)
Arquitectura Completa
Tu Código JavaScript
↓
Node.js APIs
↓
┌───────────────────────┐
│ Event Loop │
│ (libuv) │
└───────┬───────────────┘
│
┌───────┴───────────────┐
│ │
↓ ↓
Operaciones Thread Pool
Asíncronas Nativas (libuv)
(epoll/kqueue) 4 threads default
- Network I/O - File I/O
- Timers
Top comments (0)