Escribir código correcto es el primer paso; escribir código rápido es el arte. La optimización explota relaciones entre datos, cálculos y memoria. Técnicas como memoización, caching, paralelismo y análisis de complejidad transforman algoritmos lentos en rápidos.
La optimización es la disciplina de mejorar rendimiento sin sacrificar correctitud. Se basa en reconocer y eliminar redundancias en el cálculo.
Las técnicas de optimización tienen un denominador común: explotar relaciones entre cálculos:
Este tema explora técnicas prácticas de optimización y cómo reconocer cuándo aplicarlas.
La memoización es una relación entre entrada y salida que se almacena para reutilizar. Si una función pura recibe la misma entrada, devuelve el mismo resultado sin recalcular.
// Memoización manual
function fibonacci(n, memo = {}) {
if (n in memo) return memo[n];
if (n <= 1) return n;
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
return memo[n];
}
// Memoización automática con decorador
function memoizar(func) {
const cache = new Map();
return function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
return cache.get(clave);
}
const resultado = func.apply(this, args);
cache.set(clave, resultado);
return resultado;
};
}
// Uso
const fibMemo = memoizar((n) => {
if (n <= 1) return n;
return fibMemo(n - 1) + fibMemo(n - 2);
});
console.log("Fibonacci sin memoización:");
console.log(fibonacci(35)); // Lento
console.log("Fin de Fibonacci sin memoización");
console.log("Fibonacci con memoización:");
console.log(fibMemo(35)); // Rápido
console.log("Fin de Fibonacci con memoización");
// Análisis de impacto
class AnalisisRendimiento {
static memoizarConEstadísticas(func) {
const cache = new Map();
let hits = 0, misses = 0;
return {
func: function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
hits++;
return cache.get(clave);
}
misses++;
const resultado = func.apply(this, args);
cache.set(clave, resultado);
return resultado;
},
estadísticas: () => ({
hits,
misses,
tasaExito: (hits / (hits + misses) * 100).toFixed(2) + '%'
})
};
}
}
const fibStats = AnalisisRendimiento.memoizarConEstadísticas((n) => {
if (n <= 1) return n;
return fibStats.func(n - 1) + fibStats.func(n - 2);
});
fibStats.func(20);
console.log("Estadísticas:", fibStats.estadísticas());
El caching explota la localidad espacial y temporal: datos accedidos recientemente o cercanos tienden a accederse nuevamente pronto.
// Cache LRU (Least Recently Used)
class CacheLRU {
constructor(capacidad) {
this.capacidad = capacidad;
this.cache = new Map();
}
get(clave) {
if (!this.cache.has(clave)) {
return -1;
}
// Mover a final (más recientemente usado)
const valor = this.cache.get(clave);
this.cache.delete(clave);
this.cache.set(clave, valor);
return valor;
}
set(clave, valor) {
if (this.cache.has(clave)) {
this.cache.delete(clave);
} else if (this.cache.size >= this.capacidad) {
// Eliminar el primero (menos recientemente usado)
const primerKey = this.cache.keys().next().value;
this.cache.delete(primerKey);
}
this.cache.set(clave, valor);
}
estadísticas() {
return {
tamaño: this.cache.size,
capacidad: this.capacidad,
lleno: this.cache.size === this.capacidad
};
}
}
// Simulación de acceso a datos
class SistemaMemoria {
constructor(cacheSize) {
this.cache = new CacheLRU(cacheSize);
this.accesosDisco = 0;
this.accesosCache = 0;
}
acceder(direccion) {
const datos = this.cache.get(direccion);
if (datos !== -1) {
this.accesosCache++;
return datos;
}
// Simular lectura de "disco"
this.accesosDisco++;
const valor = direccion * 2; // Simulación
this.cache.set(direccion, valor);
return valor;
}
tasaAcierto() {
const total = this.accesosCache + this.accesosDisco;
if (total === 0) return 0;
return (this.accesosCache / total * 100).toFixed(2) + '%';
}
}
// Uso
const sistema = new SistemaMemoria(3);
// Patrón con localidad temporal
const secuencia = [1, 2, 3, 1, 2, 1, 4, 2, 1];
for (const dir of secuencia) {
sistema.acceder(dir);
}
console.log("Tasa de acierto de cache:", sistema.tasaAcierto());
console.log("Accesos cache:", sistema.accesosCache);
console.log("Accesos disco:", sistema.accesosDisco);
Un índice es una estructura relacional que acelera búsqueda. Un árbol B, árbol de búsqueda binaria, o tabla hash son índices que reducen búsqueda de O(n) a O(log n) o O(1).
// Comparación: búsqueda lineal vs. búsqueda binaria vs. hash
class AnalisisBusqueda {
static busquedaLineal(arr, objetivo) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === objetivo) return i;
}
return -1;
}
static busquedaBinaria(arr, objetivo) {
let izq = 0, der = arr.length - 1;
while (izq <= der) {
const mid = Math.floor((izq + der) / 2);
if (arr[mid] === objetivo) return mid;
if (arr[mid] < objetivo) izq = mid + 1;
else der = mid - 1;
}
return -1;
}
static busquedaHash(datos, objetivo) {
return datos.indexOf(objetivo) !== -1 ? 0 : -1;
}
static compararRendimiento() {
const tamano = 1000000;
const arr = Array.from({length: tamano}, (_, i) => i);
const objetivo = tamano - 1; // Peor caso: último elemento
console.log("Búsqueda lineal:");
AnalisisBusqueda.busquedaLineal(arr, objetivo);
console.log("Fin de búsqueda lineal");
console.log("Búsqueda binaria:");
AnalisisBusqueda.busquedaBinaria(arr, objetivo);
console.log("Fin de búsqueda binaria");
console.log("Búsqueda con Set (hash):");
const set = new Set(arr);
set.has(objetivo);
console.log("Fin de búsqueda con Set (hash)");
console.log("Complejidades: O(n) vs O(log n) vs O(1)");
}
}
// Índice multi-clave para consultas rápidas
class ÍndiceMultiClave {
constructor() {
this.índices = new Map();
}
crearÍndice(datos, clave) {
const índice = new Map();
for (const registro of datos) {
const valor = registro[clave];
if (!índice.has(valor)) {
índice.set(valor, []);
}
índice.get(valor).push(registro);
}
this.índices.set(clave, índice);
}
buscar(clave, valor) {
const índice = this.índices.get(clave);
if (!índice) return [];
return índice.get(valor) || [];
}
}
// Uso
const registros = [
{ id: 1, nombre: "Ana", ciudad: "Madrid" },
{ id: 2, nombre: "Luis", ciudad: "Barcelona" },
{ id: 3, nombre: "Marta", ciudad: "Madrid" },
{ id: 4, nombre: "Carlos", ciudad: "Valencia" }
];
const índice = new ÍndiceMultiClave();
índice.crearÍndice(registros, "ciudad");
console.log("Búsqueda con índice:");
const resultado = índice.buscar("ciudad", "Madrid");
console.log("Fin de búsqueda con índice");
console.log("Registros en Madrid:", resultado);
La evaluación perezosa pospone cálculos hasta que realmente se necesiten. Evita computar resultados innecesarios.
// Generadores para evaluación perezosa
function* rangoPerezoso(inicio, fin) {
for (let i = inicio; i < fin; i++) {
yield i;
}
}
function* mapPerezoso(iterable, func) {
for (const item of iterable) {
yield func(item);
}
}
function* filtroPerezoso(iterable, predicado) {
for (const item of iterable) {
if (predicado(item)) {
yield item;
}
}
}
// Sin evaluación perezosa (genera array completo)
function operacionEager(n) {
const arr = Array.from({length: n}, (_, i) => i + 1);
const mapeado = arr.map(x => x * 2);
const filtrado = mapeado.filter(x => x > 100);
return filtrado.slice(0, 5);
}
// Con evaluación perezosa (calcula solo lo necesario)
function operacionLazy(n) {
const rango = rangoPerezoso(1, n);
const mapeado = mapPerezoso(rango, x => x * 2);
const filtrado = filtroPerezoso(mapeado, x => x > 100);
const resultado = [];
let count = 0;
for (const item of filtrado) {
resultado.push(item);
if (++count === 5) break;
}
return resultado;
}
console.log("Eager evaluation:");
const eagerResult = operacionEager(1000000);
console.log("Fin de eager evaluation");
console.log("Lazy evaluation:");
const lazyResult = operacionLazy(1000000);
console.log("Fin de lazy evaluation");
console.log("Resultados iguales:",
JSON.stringify(eagerResult) === JSON.stringify(lazyResult));
El paralelismo explota la independencia entre tareas para ejecutarlas simultáneamente. Reduce tiempo total diviendo trabajo entre múltiples procesadores.
// Procesamiento paralelo con Web Workers (simulado en Node.js con promesas)
class ProcesadorParalelo {
constructor(numHilos = 4) {
this.numHilos = numHilos;
}
// Dividir array en chunks para procesamiento paralelo
dividirEnChunks(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
// Simular procesamiento paralelo
async procesarParalelo(datos, operacion) {
const chunkSize = Math.ceil(datos.length / this.numHilos);
const chunks = this.dividirEnChunks(datos, chunkSize);
// Crear promesas para cada chunk
const promesas = chunks.map(chunk =>
new Promise(resolve => {
setImmediate(() => {
const resultado = chunk.map(operacion);
resolve(resultado);
});
})
);
// Esperar a que terminen todas
const resultados = await Promise.all(promesas);
// Combinar resultados
return resultados.flat();
}
// Procesamiento secuencial (para comparar)
procesarSecuencial(datos, operacion) {
return datos.map(operacion);
}
}
// Uso
const procesador = new ProcesadorParalelo(4);
const datos = Array.from({length: 1000000}, (_, i) => i);
// Operación: calcular cuadrado
const operacion = (x) => {
let suma = 0;
for (let i = 0; i < 1000; i++) {
suma += Math.sqrt(x + i);
}
return suma;
};
console.log("Procesamiento secuencial:");
const resultSecuencial = procesador.procesarSecuencial(datos.slice(0, 1000), operacion);
console.log("Fin de procesamiento secuencial");
console.log("Procesamiento paralelo:");
procesador.procesarParalelo(datos.slice(0, 1000), operacion).then(() => {
console.log("Fin de procesamiento paralelo");
});
// Pool de tareas
class PoolTareas {
constructor(numWorkers) {
this.numWorkers = numWorkers;
this.colaEspera = [];
this.activos = 0;
}
async ejecutar(tarea) {
if (this.activos < this.numWorkers) {
this.activos++;
try {
return await tarea();
} finally {
this.activos--;
this.procesarSiguiente();
}
} else {
return new Promise(resolve => {
this.colaEspera.push(async () => {
try {
resolve(await tarea());
} finally {
this.activos--;
this.procesarSiguiente();
}
});
});
}
}
procesarSiguiente() {
if (this.colaEspera.length > 0 && this.activos < this.numWorkers) {
this.activos++;
const tareaFunc = this.colaEspera.shift();
tareaFunc();
}
}
}
// Uso del pool
const pool = new PoolTareas(2);
for (let i = 0; i < 5; i++) {
pool.ejecutar(async () => {
console.log(`Tarea ${i} iniciada`);
await new Promise(r => setTimeout(r, 100));
console.log(`Tarea ${i} completada`);
});
}
No puedes optimizar lo que no mides. El profiling identifica cuellos de botella reales, no especulados.
// Herramienta de profiling simple
class Profiler {
constructor() {
this.mediciones = new Map();
}
marcar(etiqueta) {
if (!this.mediciones.has(etiqueta)) {
this.mediciones.set(etiqueta, []);
}
return performance.now();
}
terminar(etiqueta, inicio) {
const fin = performance.now();
const duracion = fin - inicio;
this.mediciones.get(etiqueta).push(duracion);
return duracion;
}
estadísticas(etiqueta) {
const tiempos = this.mediciones.get(etiqueta) || [];
if (tiempos.length === 0) return null;
const suma = tiempos.reduce((a, b) => a + b, 0);
const promedio = suma / tiempos.length;
const minimo = Math.min(...tiempos);
const maximo = Math.max(...tiempos);
return {
llamadas: tiempos.length,
promedio: promedio.toFixed(3),
minimo: minimo.toFixed(3),
maximo: maximo.toFixed(3),
total: suma.toFixed(3)
};
}
reporte() {
console.log("=== Reporte de Profiling ===");
for (const [etiqueta, tiempos] of this.mediciones) {
console.log(`${etiqueta}:`, this.estadísticas(etiqueta));
}
}
}
// Uso
const profiler = new Profiler();
function funcionLenta() {
let suma = 0;
for (let i = 0; i < 1000000; i++) {
suma += Math.sqrt(i);
}
return suma;
}
function funcionRapida() {
return "rápido";
}
// Medir
for (let i = 0; i < 5; i++) {
let inicio = profiler.marcar("lenta");
funcionLenta();
profiler.terminar("lenta", inicio);
inicio = profiler.marcar("rápida");
funcionRapida();
profiler.terminar("rápida", inicio);
}
profiler.reporte();
// Identificar punto caliente
class AnalizadorRendimiento {
static identificarCuelloBotel(funciones) {
const resultados = [];
for (const [nombre, func] of Object.entries(funciones)) {
const inicio = performance.now();
for (let i = 0; i < 1000; i++) {
func();
}
const duracion = performance.now() - inicio;
resultados.push({ nombre, duracion });
}
resultados.sort((a, b) => b.duracion - a.duracion);
console.log("Funciones por tiempo total:");
resultados.forEach(r => {
console.log(` ${r.nombre}: ${r.duracion.toFixed(2)}ms`);
});
return resultados[0].nombre; // Función más lenta
}
}
const candidatos = {
fibonacci: () => {
let a = 0, b = 1;
for (let i = 0; i < 20; i++) {
[a, b] = [b, a + b];
}
return b;
},
sumaArray: () => {
const arr = Array.from({length: 100}, (_, i) => i);
return arr.reduce((a, b) => a + b, 0);
},
busquedaLineal: () => {
const arr = Array.from({length: 1000}, (_, i) => i);
return arr.indexOf(500);
}
};
console.log("\nCuello de botella:", AnalizadorRendimiento.identificarCuelloBotel(candidatos));
La optimización es la disciplina de explotar relaciones en datos y cálculos para acelerar programas sin sacrificar correctitud.
Hemos explorado cómo cada técnica de optimización se basa en una observación relacional:
La lección más importante: no optimices por especulación, mide y dirige tu esfuerzo donde realmente cuenta. El 80% del tiempo se consume en 20% del código. Encuentra ese 20% con profiling, luego aplica las técnicas apropiadas.
Ahora que has completado este curso, tienes el marco conceptual para entender cualquier técnica de optimización que encuentres: pregúntate qué relación está siendo explotada. ¿Es orden? ¿Es proximidad? ¿Es determinismo? ¿Es independencia? La respuesta te dirá cuándo aplicar cada técnica correctamente.
Las relaciones discretas son el lenguaje subyacente de toda la programación eficiente.