Inicio / TypeScript / Conceptos de Backend / Performance y Escalabilidad

Performance y Escalabilidad

Big O, N+1 queries, connection pooling, paginación por cursor y procesamiento paralelo.

Avanzado
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Performance y Escalabilidad en Backend

El rendimiento es un requisito no funcional crítico. Antes de optimizar, siempre mide. La optimización prematura es la raíz de muchos males.


Complejidad algorítmica (Big O)

Entender Big O es fundamental para predecir cómo escala tu código.

// O(1) — tiempo constante
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];  // siempre igual de rápido, sin importar el tamaño
}

// O(log n) — logarítmico (búsqueda binaria)
function binarySearch(sorted: number[], target: number): number {
  let low = 0, high = sorted.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (sorted[mid] === target) return mid;
    if (sorted[mid] < target)  low  = mid + 1;
    else                        high = mid - 1;
  }
  return -1;
}

// O(n) — lineal
function findUser(users: User[], email: string): User | undefined {
  return users.find(u => u.email === email);
}

// O(n²) — cuadrático (evitar con n > 1000)
function hasDuplicates(arr: string[]): boolean {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] === arr[j]) return true;  // ❌ O(n²)
    }
  }
  return false;
}

// ✅ O(n) — usando Set
function hasDuplicatesFast(arr: string[]): boolean {
  const seen = new Set<string>();
  for (const item of arr) {
    if (seen.has(item)) return true;
    seen.add(item);
  }
  return false;
}

Optimización de consultas a BD

El problema N+1 (revisión práctica)

// ❌ N+1: 1 query para usuarios + N queries para pedidos
async function getReportSlow(userIds: number[]) {
  const users = await userRepo.findByIds(userIds);       // 1 query
  const result = [];

  for (const user of users) {
    const orders = await orderRepo.findByUser(user.id);  // N queries!
    result.push({ user, orderCount: orders.length, total: orders.reduce(...) });
  }
  return result;
}

// ✅ 2 queries: mucho más eficiente
async function getReportFast(userIds: number[]) {
  const [users, orders] = await Promise.all([
    userRepo.findByIds(userIds),
    orderRepo.findByUserIds(userIds),   // WHERE user_id IN (...)
  ]);

  const ordersByUser = Map.groupBy(orders, o => o.userId);  // O(n)

  return users.map(user => {
    const userOrders = ordersByUser.get(user.id) ?? [];
    return {
      user,
      orderCount: userOrders.length,
      total: userOrders.reduce((s, o) => s + o.totalCents, 0),
    };
  });
}

Índices de BD

-- Identifica queries lentas
-- PostgreSQL: pg_stat_statements
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

-- Identifica índices faltantes
SELECT schemaname, tablename, seq_scan, idx_scan, n_live_tup
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
ORDER BY seq_scan DESC;

-- Índice covering: incluye todas las columnas de la query
CREATE INDEX idx_orders_status_covering ON orders(status, created_at)
INCLUDE (user_id, total_cents);
-- → la query puede resolverse solo con el índice, sin ir a la tabla

Connection Pooling

import { Pool } from 'pg';

// ❌ Nueva conexión por petición (costoso: ~10-50ms por conexión)
app.get('/users', async (req, res) => {
  const client = new Client({ connectionString: process.env.DATABASE_URL });
  await client.connect();    // lento
  const { rows } = await client.query('SELECT * FROM users');
  await client.end();        // libera la conexión
  res.json(rows);
});

// ✅ Pool: reutiliza conexiones
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max:              20,     // máximo 20 conexiones simultáneas
  min:              2,      // mínimo 2 conexiones en espera
  idleTimeoutMillis: 30000, // cierra conexiones inactivas después de 30s
  connectionTimeoutMillis: 5000,  // error si no hay conexión disponible en 5s
});

app.get('/users', async (req, res) => {
  const { rows } = await pool.query('SELECT * FROM users');
  res.json(rows);
  // La conexión vuelve automáticamente al pool
});

Paginación eficiente

// ❌ OFFSET es O(n): escanea y descarta las primeras n filas
const page10000 = await db.query(
  'SELECT * FROM events ORDER BY id LIMIT 20 OFFSET 200000'
);
// PostgreSQL debe leer 200.020 filas para devolver 20

// ✅ Cursor pagination: siempre O(log n) con índice
interface CursorPage<T> {
  data:    T[];
  cursor:  string | null;  // ID del último elemento, opaco para el cliente
  hasMore: boolean;
}

async function getEventsCursor(
  afterId: number | null,
  limit = 20
): Promise<CursorPage<Event>> {
  const where = afterId ? `AND id > ${afterId}` : '';
  const rows  = await db.query(`
    SELECT * FROM events
    WHERE 1=1 ${where}
    ORDER BY id ASC
    LIMIT ${limit + 1}     -- pide uno más para saber si hay siguiente página
  `);

  const hasMore = rows.length > limit;
  const data    = rows.slice(0, limit);

  return {
    data,
    cursor:  hasMore ? String(data[data.length - 1].id) : null,
    hasMore,
  };
}

Procesamiento en paralelo

// ❌ Secuencial: 300ms + 200ms + 150ms = 650ms
async function getUserDashboardSlow(userId: number) {
  const user     = await userRepo.findById(userId);      // 300ms
  const orders   = await orderRepo.findByUser(userId);   // 200ms
  const notifs   = await notifRepo.findByUser(userId);   // 150ms
  return { user, orders, notifications: notifs };
}

// ✅ Paralelo con Promise.all: max(300, 200, 150) = 300ms
async function getUserDashboardFast(userId: number) {
  const [user, orders, notifs] = await Promise.all([
    userRepo.findById(userId),
    orderRepo.findByUser(userId),
    notifRepo.findByUser(userId),
  ]);
  return { user, orders, notifications: notifs };
}

// Promise.allSettled: continúa aunque alguno falle
async function getUserDashboardResilient(userId: number) {
  const results = await Promise.allSettled([
    userRepo.findById(userId),
    orderRepo.findByUser(userId),
    notifRepo.findByUser(userId),
  ]);

  return {
    user:   results[0].status === 'fulfilled' ? results[0].value : null,
    orders: results[1].status === 'fulfilled' ? results[1].value : [],
    notifs: results[2].status === 'fulfilled' ? results[2].value : [],
  };
}

Streaming de respuestas grandes

import { createReadStream } from 'fs';
import { Transform } from 'stream';

// ❌ Carga todo en memoria
app.get('/export/users', async (req, res) => {
  const users = await db.query('SELECT * FROM users');  // puede ser GB
  res.json(users);                                       // OOM para datasets grandes
});

// ✅ Streaming: memoria O(1)
app.get('/export/users', (req, res) => {
  res.setHeader('Content-Type', 'application/x-ndjson');  // newline-delimited JSON

  const cursor = db.queryCursor('SELECT * FROM users ORDER BY id');

  cursor.on('data', (row) => {
    res.write(JSON.stringify(row) + '\n');
  });

  cursor.on('end', () => res.end());
  cursor.on('error', (err) => res.destroy(err));
});

Métricas clave de performance

Métrica Objetivo típico Herramienta
Latencia P50 < 50ms Prometheus, Datadog
Latencia P99 < 500ms Prometheus, Datadog
Throughput > 1000 req/s (depende del sistema) k6, Artillery
Error rate < 0.1% Sentry, Datadog
DB query time < 50ms para P99 pg_stat_statements
Memory usage < 80% del límite del proceso process.memoryUsage()
// Middleware para medir latencia de cada request
app.use((req, res, next) => {
  const start = process.hrtime.bigint();

  res.on('finish', () => {
    const durationMs = Number(process.hrtime.bigint() - start) / 1_000_000;
    metrics.histogram('http.request.duration', durationMs, {
      method:   req.method,
      route:    req.route?.path ?? 'unknown',
      status:   String(res.statusCode),
    });
  });

  next();
});
🔒

Ejercicio práctico disponible

Optimización: N+1 y Memoización

Desbloquear ejercicios
// Optimización: N+1 y Memoización
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes