La regla de oro: el trabajo más rápido es el que no haces
Optimizar el rendimiento es, casi siempre, evitar trabajo repetido: no recalcular, no volver a pedir, no enviar lo que ya está. La herramienta central para eso es la caché, y conviene pensarla en niveles.
Caché por niveles
Navegador / HTTP: el cliente guarda respuestas según las cabeceras
Cache-ControlyETag. Si el recurso no cambió, el servidor responde304 Not Modifiedy no reenvía el cuerpo. Coste de red: cero.CDN para estáticos: una red de servidores cercanos al usuario sirve imágenes, JS y CSS desde el borde, sin tocar tu origin. Más cerca = menos latencia y menos carga en tu backend.
Caché en el backend (Redis) con el patrón cache-aside:
¿está en caché? → sí → devuélvelo (rápido) → no → ve a la BD, guarda el resultado en caché, devuélveloLa aplicación gestiona la caché "al lado" de la BD. Hay que decidir el TTL (cuánto vive el dato) e invalidar la entrada cuando el dato cambia, o servirás datos viejos. Invalidar una caché es uno de los problemas difíciles de verdad.
En el frontend: enviar menos y más tarde
- Lazy loading: carga las cosas cuando se necesitan, no al arrancar (imágenes al hacer scroll, una ruta al visitarla).
- Code splitting: parte el bundle en trozos y descarga solo el que la pantalla actual necesita. El usuario no paga por código que aún no usa.
- Compresión: el servidor comprime las respuestas de texto (HTML, JS, CSS, JSON) con gzip o brotli (mejor ratio) antes de enviarlas. Un JS de 300 KB puede viajar como 80 KB.
En la base de datos: índices y el problema N+1
La BD suele ser el cuello de botella. Dos clásicos:
- Índices: sin índice, buscar una fila implica recorrer toda la tabla
(full scan). Un índice sobre la columna por la que filtras (
WHERE email = ?) convierte ese recorrido en una búsqueda casi instantánea. Cuestan algo en escritura y espacio, así que se indexan las columnas por las que se filtra y se ordena, no todas. - El problema N+1: pides una lista de N elementos (1 consulta) y luego, por
cada uno, haces otra consulta para sus datos relacionados. Resultado: 1 + N
consultas. Con 100 pedidos son 101 viajes a la BD. La solución es agrupar
(batching): cargar los datos de los N en una sola consulta
(
WHERE id IN (...)) o con unJOIN.
Mide antes de optimizar. "Creo que esto es lento" no es un dato; un p95 y una traza, sí. Optimizar a ciegas añade complejidad sin garantía de mejora.
Ejemplos
Cache-aside con un Map: no recalcular lo ya calculado
function cachear(fn) {
const cache = new Map();
return function (clave) {
if (cache.has(clave)) return cache.get(clave);
const valor = fn(clave);
cache.set(clave, valor);
return valor;
};
}
let llamadas = 0;
const cuadrado = cachear((n) => { llamadas++; return n * n; });
cuadrado(8); cuadrado(8); cuadrado(8);
console.log("resultado:", cuadrado(8), "llamadas reales:", llamadas); // 64, 1
N+1 vs. batching: 1 consulta en lugar de N
// Simula una carga "en bloque" de varios ids de una vez.
function cargarUsuarios(ids) {
console.log("consultas a la BD:", 1, "para", ids.length, "ids");
return ids.map((id) => ({ id, nombre: "Usuario " + id }));
}
const ids = [1, 2, 3, 4, 5];
const usuarios = cargarUsuarios(ids); // 1 viaje, no 5
console.log(usuarios.length, "usuarios cargados");