Node.js puede manejar decenas de miles de conexiones simultaneas en un solo servidor. Esta guia cubre clustering, streams, profiling y estrategias de caching.
Comprender el bucle de eventos
Node.js es de un solo hilo. Bloquear el bucle de eventos bloquea todas las solicitudes.
// NEVER do this — blocks the event loop
app.get('/compute', (req, res) => {
// Synchronous CPU-heavy computation blocks ALL requests
let result = 0;
for (let i = 0; i < 1e9; i++) result += i; // 1 billion iterations!
res.json({ result });
});
// DO THIS instead — offload to worker thread
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
app.get('/compute', (req, res) => {
const worker = new Worker('./computeWorker.js', {
workerData: { input: req.query.n }
});
worker.on('message', result => res.json({ result }));
worker.on('error', err => res.status(500).json({ error: err.message }));
});Clustering para rendimiento multi-nucleo
Node.js se ejecuta en un solo nucleo de CPU por defecto.
// Node.js Cluster Module — Use All CPU Cores
const cluster = require('cluster');
const os = require('os');
const express = require('express');
const NUM_WORKERS = os.cpus().length;
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
console.log(`Starting ${NUM_WORKERS} workers...`);
// Fork workers
for (let i = 0; i < NUM_WORKERS; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
cluster.fork(); // Auto-restart crashed workers
});
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`);
});
} else {
// Worker process — runs the actual server
const app = express();
app.get('/api/users', async (req, res) => {
const users = await db.getUsers();
res.json(users);
});
app.listen(3000, () => {
console.log(`Worker ${process.pid} listening on port 3000`);
});
}
// Alternative: PM2 cluster mode (recommended for production)
// pm2 start server.js -i max # auto-detect CPU count
// pm2 start server.js -i 4 # explicit countStreams para eficiencia de memoria
Los streams permiten procesar datos pieza por pieza sin cargar todo en memoria.
// Node.js Streams — Memory-Efficient Processing
const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);
// 1. Stream a large file as HTTP response (no memory buffering)
app.get('/download/large-file', (req, res) => {
const filePath = './large-file.csv';
const stat = fs.statSync(filePath);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Disposition', 'attachment; filename=data.csv');
// Pipe file directly to response — never fully in memory
fs.createReadStream(filePath).pipe(res);
});
// 2. Transform stream for CSV processing
class CsvParser extends Transform {
constructor() {
super({ objectMode: true });
this.buffer = '';
this.headers = null;
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split('\n');
this.buffer = lines.pop(); // Keep incomplete line in buffer
for (const line of lines) {
if (!this.headers) {
this.headers = line.split(',');
continue;
}
const values = line.split(',');
const record = {};
this.headers.forEach((h, i) => record[h.trim()] = values[i]?.trim());
this.push(record);
}
callback();
}
}
// 3. Pipeline for reliable error handling
async function processLargeCsvFile(inputPath, outputPath) {
await pipelineAsync(
fs.createReadStream(inputPath),
new CsvParser(),
new Transform({
objectMode: true,
transform(record, enc, cb) {
// Transform each record
record.processed = true;
cb(null, JSON.stringify(record) + '\n');
}
}),
fs.createWriteStream(outputPath)
);
console.log('Processing complete');
}Estrategias de caching
El caching es la optimizacion de rendimiento de mayor impacto.
// Caching Strategies for Node.js
// 1. In-Memory LRU Cache
const { LRUCache } = require('lru-cache');
const cache = new LRUCache({
max: 500, // Maximum 500 items
ttl: 5 * 60 * 1000, // 5 minutes TTL
allowStale: true, // Return stale value while refreshing
updateAgeOnGet: true,
});
async function getUser(id) {
const cacheKey = `user:${id}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
const user = await db.findUser(id);
cache.set(cacheKey, user);
return user;
}
// 2. Redis Cache with Stale-While-Revalidate
const Redis = require('ioredis');
const redis = new Redis();
async function getCachedData(key, fetchFn, ttl = 300) {
const [cached, ttlRemaining] = await redis.pipeline()
.get(key)
.ttl(key)
.exec();
if (cached[1]) {
const data = JSON.parse(cached[1]);
// Background refresh when < 60 seconds remaining
if (ttlRemaining[1] < 60) {
fetchFn().then(fresh =>
redis.setex(key, ttl, JSON.stringify(fresh))
);
}
return data;
}
const data = await fetchFn();
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
// 3. HTTP Response Caching with ETags
app.get('/api/products', async (req, res) => {
const products = await getProducts();
const etag = require('crypto')
.createHash('md5')
.update(JSON.stringify(products))
.digest('hex');
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
res.json(products);
});Preguntas frecuentes
Cuantos workers de cluster crear?
Crea un worker por nucleo de CPU, os.cpus().length workers.
Cuando usar streams vs cargar en memoria?
Usa streams para archivos mayores de 10 MB, piping de datos y procesamiento incremental.
Que es el flag --inspect?
El flag --inspect inicia Node.js con el protocolo V8 inspector habilitado.
Por que mi app Node.js usa tanta memoria?
Causas comunes: fugas de memoria, caches sin eviction, grandes conjuntos de datos en memoria.