Tiempos de respuesta API Excel: De 5 segundos a 50 milisegundos

El problema de 5 segundos que casi mata nuestro producto

Nuestra primera demo de API Excel fue un desastre.

Cliente: «Muéstrame qué tan rápido calcula nuestros precios.»

Nosotros: «¡Claro!» hace clic en el botón

Indicador de carga: 🔄... 🔄... 🔄... 🔄... 🔄...

5,2 segundos después: «¡Aquí está tu precio!»

Cliente: «Nos quedaremos con nuestra solución actual.»

Ese día aprendimos que nadie espera 5 segundos por un cálculo. Así es como lo redujimos a 50ms.

La anatomía de una llamada API Excel lenta

Desglosemos dónde se iban esos 5 segundos:

Tiempo de respuesta original: 5.247ms
├── Análisis de petición HTTP: 23ms (0,4%)
├── Autenticación: 89ms (1,7%)
├── Carga de archivo Excel: 1.832ms (34,9%) ⚠️
├── Actualización de celdas de entrada: 467ms (8,9%)
├── Ejecución del cálculo: 2.234ms (42,6%) ⚠️
├── Extracción de salidas: 312ms (5,9%)
├── Formato de respuesta: 178ms (3,4%)
└── Respuesta de red: 112ms (2,1%)

Los culpables: La carga del archivo y la ejecución del cálculo devoraban el 77,5% de nuestro tiempo.

Paso 1: Mantener Excel caliente (1.832ms → 0ms)

El problema

Cada llamada API cargaba Excel desde el disco:

//  La forma lenta
async function calcularPrecio(entradas) {
  const excel = await cargarArchivoExcel('precios.xlsx'); // ¡1,8 segundos!
  await excel.establecerEntradas(entradas);
  await excel.calcular();
  return excel.obtenerSalidas();
}

La solución: Pool de procesos

//  La forma rápida
class PoolProcesosExcel {
  constructor(config) {
    this.procesos = [];
    this.disponibles = [];
    this.esperando = [];
  }
  
  async inicializar() {
    // Pre-cargar procesos Excel al inicio
    for (let i = 0; i < this.config.tamañoPool; i++) {
      const proceso = await this.crearProcesoExcel();
      await proceso.cargarLibro(this.config.rutaLibro);
      this.procesos.push(proceso);
      this.disponibles.push(proceso);
    }
  }
  
  async ejecutar(entradas) {
    // Obtener un proceso Excel ya cargado
    const proceso = await this.obtenerProcesoDisponible(); // ¡0ms!
    
    try {
      return await proceso.calcular(entradas);
    } finally {
      this.liberarProceso(proceso);
    }
  }
}

Resultado: Tiempo de carga de archivo: 1.832ms → 0ms

Paso 2: Caché inteligente (2.234ms → 8ms para hits de caché)

El problema

Recalcular entradas idénticas:

// Escenario común: Usuario ajustando cantidad
obtenerPrecio({ producto: 'A', cantidad: 100 }); // 2,2s
obtenerPrecio({ producto: 'A', cantidad: 101 }); // 2,2s
obtenerPrecio({ producto: 'A', cantidad: 102 }); // 2,2s
obtenerPrecio({ producto: 'A', cantidad: 100 }); // 2,2s (¡ya visto!)

La solución: Caché multicapa

class CacheInteligente {
  constructor() {
    // Capa 1: Caché en memoria (el más rápido)
    this.cacheMemoria = new LRU({ 
      max: 10000, 
      ttl: 5 * 60 * 1000 // 5 minutos
    });
    
    // Capa 2: Caché Redis (compartido entre instancias)
    this.cacheRedis = new RedisClient({
      ttl: 30 * 60 * 1000 // 30 minutos
    });
    
    // Capa 3: Huella digital de cálculo
    this.cacheHuella = new Map();
  }
  
  async obtener(entradas) {
    const clave = this.generarClave(entradas);
    
    // Verificar caché de memoria primero (< 1ms)
    const resultadoMemoria = this.cacheMemoria.get(clave);
    if (resultadoMemoria) return resultadoMemoria;
    
    // Verificar caché Redis (5-10ms)
    const resultadoRedis = await this.cacheRedis.get(clave);
    if (resultadoRedis) {
      this.cacheMemoria.set(clave, resultadoRedis);
      return resultadoRedis;
    }
    
    // Verificar si hemos visto un cálculo similar
    const huella = this.generarHuella(entradas);
    const similar = this.cacheHuella.get(huella);
    if (similar && this.puedeReutilizarSimilar(entradas, similar)) {
      return this.ajustarResultadoSimilar(similar, entradas);
    }
    
    return null;
  }
  
  generarHuella(entradas) {
    // Huella inteligente para cálculos similares
    return `${entradas.producto}-${Math.floor(entradas.cantidad / 10) * 10}`;
  }
}

Tasas de acierto del caché:

  • Caché de memoria: 45% de aciertos (< 1ms)
  • Caché Redis: 30% de aciertos (8ms)
  • Cálculo fresco: 25% (varía)

Paso 3: Procesamiento paralelo (467ms → 89ms)

El problema

Actualizaciones secuenciales de celdas:

//  Actualizaciones secuenciales lentas
await excel.establecerCelda('B2', entradas.cantidad);    // 93ms
await excel.establecerCelda('B3', entradas.producto);    // 93ms
await excel.establecerCelda('B4', entradas.cliente);     // 93ms
await excel.establecerCelda('B5', entradas.region);      // 93ms
await excel.establecerCelda('B6', entradas.moneda);      // 93ms
// Total: 465ms

La solución: Actualizaciones por lotes

//  Actualización rápida por lotes
class ActualizadorPorLotes {
  async actualizarCeldas(excel, actualizaciones) {
    // Preparar todas las actualizaciones
    const loteActualizacion = Object.entries(actualizaciones).map(([celda, valor]) => ({
      celda,
      valor,
      tipo: this.detectarTipo(valor)
    }));
    
    // Ordenar por localidad para eficiencia de caché
    loteActualizacion.sort((a, b) => {
      const filaA = parseInt(a.celda.substring(1));
      const filaB = parseInt(b.celda.substring(1));
      return filaA - filaB;
    });
    
    // Ejecutar como operación única
    await excel.actualizacionPorLote(loteActualizacion); // ¡89ms en total!
  }
}

Paso 4: Optimización de cálculos (2.234ms → 234ms)

El problema

Calcular el libro completo:

// Libro con 50 hojas, 10.000 fórmulas
// ¡Pero solo necesitamos resultados de Hoja1!A1:A10!

La solución: Cálculo selectivo

class CalculoInteligente {
  constructor(libro) {
    this.libro = libro;
    this.grafoDependencias = this.construirGrafoDependencias();
  }
  
  async calcular(entradas, salidasRequeridas) {
    // 1. Identificar celdas afectadas
    const celdasAfectadas = this.obtenerCeldasAfectadas(entradas);
    
    // 2. Encontrar dependencias de salidas requeridas
    const dependencias = this.obtenerDependencias(salidasRequeridas);
    
    // 3. Calcular solo la intersección
    const celdasACalcular = this.intersectar(celdasAfectadas, dependencias);
    
    // 4. Cálculo selectivo
    if (celdasACalcular.length < 100) {
      // Calcular solo celdas específicas
      await this.libro.calcularCeldas(celdasACalcular); // 234ms
    } else {
      // Volver al cálculo completo
      await this.libro.calcularCompleto(); // 2234ms
    }
  }
  
  construirGrafoDependencias() {
    // Construir grafo de dependencias de fórmulas
    const grafo = new Map();
    
    this.libro.formulas.forEach(formula => {
      const deps = this.extraerDependencias(formula);
      grafo.set(formula.celda, deps);
    });
    
    return grafo;
  }
}

Paso 5: Optimización de respuesta (312ms → 47ms)

El problema

Extraer todas las salidas posibles:

//  Extraer todo
const salidas = {
  precio: excel.obtenerCelda('E10'),
  descuento: excel.obtenerCelda('E11'),
  impuesto: excel.obtenerCelda('E12'),
  envio: excel.obtenerCelda('E13'),
  // ... 50 campos más que podrían no necesitarse
};

La solución: Carga perezosa de salidas

//  Extracción inteligente de salidas
class ExtractorSalidaPerezoso {
  constructor(excel, mapeoSalidas) {
    this.excel = excel;
    this.mapeo = mapeoSalidas;
    this.cache = new Map();
  }
  
  obtenerSalida() {
    // Devolver proxy que carga al acceder
    return new Proxy({}, {
      get: (target, prop) => {
        if (this.cache.has(prop)) {
          return this.cache.get(prop);
        }
        
        if (this.mapeo[prop]) {
          const valor = this.excel.obtenerCelda(this.mapeo[prop]);
          this.cache.set(prop, valor);
          return valor;
        }
        
        return undefined;
      }
    });
  }
}

// Uso
const resultado = extractor.obtenerSalida();
// Solo carga cuando se accede:
console.log(resultado.precio); // Carga E10
// No carga otros campos a menos que se necesiten

Paso 6: Optimización de infraestructura

Distribución geográfica

class DespliegueEdge {
  constructor() {
    this.regiones = {
      'eu-oeste': { url: 'https://eu-oeste.spreadapi.com', latencia: 15 },
      'eu-central': { url: 'https://eu-central.spreadapi.com', latencia: 10 },
      'us-este': { url: 'https://us-este.spreadapi.com', latencia: 25 }
    };
  }
  
  async ejecutar(entradas, regionUsuario) {
    // Enrutar al edge más cercano
    const edge = this.obtenerEdgeMasCercano(regionUsuario);
    
    // Intentar edge primario
    try {
      return await this.llamarEdge(edge, entradas);
    } catch (error) {
      // Recurrir al siguiente más cercano
      return await this.llamarEdgeRespaldo(regionUsuario, entradas);
    }
  }
}

Pool de conexiones

//  Reutilizar conexiones
const sesionHttp2 = http2.connect('https://api.spreadapi.com', {
  peerMaxConcurrentStreams: 100
});

// Múltiples peticiones sobre la misma conexión
const peticiones = entradas.map(entrada => 
  hacerPeticion(sesionHttp2, entrada)
);

La arquitectura final

Tiempo de respuesta optimizado: 47ms promedio
├── Análisis petición: 2ms (4,3%)
├── Verificación caché: 1ms (2,1%)
├── Selección proceso: 0ms (0%)
├── Actualizaciones entrada: 8ms (17%)
├── Cálculo: 23ms (48,9%)
├── Extracción salida: 5ms (10,6%)
├── Formato respuesta: 3ms (6,4%)
└── Red: 5ms (10,6%)

Tiempo respuesta hit caché: 8ms
├── Análisis petición: 2ms
├── Búsqueda caché: 3ms
├── Formato respuesta: 1ms
└── Red: 2ms

Métricas de rendimiento del mundo real

Antes de la optimización

  • Respuesta promedio: 5.247ms
  • Respuesta P95: 8.234ms
  • Respuesta P99: 12.453ms
  • Peticiones/segundo: 3,2
  • Uso de CPU: 95%
  • Uso de memoria: 4,2GB

Después de la optimización

  • Respuesta promedio: 47ms (111x más rápido)
  • Respuesta P95: 89ms
  • Respuesta P99: 234ms
  • Peticiones/segundo: 847 (265x más)
  • Uso de CPU: 45%
  • Uso de memoria: 2,8GB

Lista de verificación de implementación

Ganancias rápidas (1 día)

  • [ ] Habilitar pool de procesos
  • [ ] Agregar caché de memoria básico
  • [ ] Agrupar actualizaciones de celdas
  • [ ] Habilitar HTTP/2

Esfuerzo medio (1 semana)

  • [ ] Implementar caché Redis
  • [ ] Construir grafo de dependencias
  • [ ] Agregar cálculo selectivo
  • [ ] Desplegar en múltiples regiones

Avanzado (1 mes)

  • [ ] Caché basado en huella digital
  • [ ] Pre-cálculo predictivo
  • [ ] Motor de cálculo Excel personalizado
  • [ ] Despliegue edge computing

Errores comunes a evitar

1. Sobre-cachear

//  Incorrecto: Cachear todo para siempre
cache.set(clave, resultado, { ttl: Infinity });

//  Correcto: Expiración inteligente
cache.set(clave, resultado, { 
  ttl: resultado.esVolatil ? 60000 : 300000 
});

2. Sub-pooling

//  Incorrecto: Un proceso para todas las peticiones
const pool = new PoolExcel({ tamaño: 1 });

//  Correcto: Tamaño basado en carga
const pool = new PoolExcel({ 
  tamaño: Math.max(4, os.cpus().length),
  tamañoMax: 16
});

3. Ignorar los internos de Excel

//  Incorrecto: Forzar recálculo completo
excel.forzarRecalculoCompleto();

//  Correcto: Dejar que Excel optimice
excel.establecerModoCalculo('automatico');
excel.habilitarCalculoIterativo();

Monitoreo y depuración

Métricas clave a rastrear

class MonitorRendimiento {
  rastrearPeticion(idPeticion) {
    return {
      inicio: Date.now(),
      marcas: new Map(),
      
      marcar(nombre) {
        this.marcas.set(nombre, Date.now());
      },
      
      finalizar() {
        const duracion = Date.now() - this.inicio;
        
        // Enviar a monitoreo
        metricas.histograma('api.tiempo_respuesta', duracion);
        metricas.incrementar('api.peticiones');
        
        // Rastrear rendimiento del caché
        if (this.marcas.has('cache_hit')) {
          metricas.incrementar('cache.aciertos');
        } else {
          metricas.incrementar('cache.fallos');
        }
        
        // Registrar peticiones lentas
        if (duracion > 100) {
          logger.warn('Petición lenta', {
            idPeticion,
            duracion,
            desglose: Array.from(this.marcas.entries())
          });
        }
      }
    };
  }
}

El impacto empresarial

Feedback del cliente

Antes: «Es preciso pero demasiado lento para producción.»

Después: «¡Más rápido que nuestra aplicación nativa!»

Métricas técnicas

  • Errores de timeout API: 15% → 0%
  • Abandono de clientes por rendimiento: 30% → 2%
  • Costos de infraestructura: Reducidos en 60%
  • Felicidad del desarrollador: 📈

Tus próximos pasos

  1. Medir primero: Perfila tus tiempos de respuesta API actuales
  2. Elegir las frutas bajas: Comienza con pool de procesos y caché básico
  3. Iterar: Cada optimización construye sobre la anterior
  4. Monitorear: Rastrea mejoras y regresiones

Recuerda: Los usuarios esperan respuestas instantáneas. 5 segundos bien podrían ser una eternidad. ¿Pero 50ms? Ese es el punto dulce donde los cálculos de Excel se sienten instantáneos.

Haz tus APIs de Excel rápidas con SpreadAPI - Ya hemos hecho la optimización por ti.

P.D. - ¿Ese cliente que se alejó de nuestra demo de 5 segundos? Ahora es nuestro mayor cliente empresarial. Resulta que 50ms marcan toda la diferencia.