tutoriales.com

Consumo Eficiente de APIs con JavaScript: AbortController y Request Caching

Descubre cómo cancelar peticiones HTTP en vuelo usando AbortController y cómo implementar estrategias de caché inteligentes en JavaScript moderno para crear aplicaciones web rápidas y eficientes.

Intermedio8 min de lectura4 views
Reportar error

Introducción al Rendimiento en Red 🚀

Cuando desarrollamos aplicaciones web modernas, la comunicación con servidores externos mediante APIs es una tarea fundamental. Sin embargo, un consumo descontrolado de peticiones HTTP puede arruinar la experiencia de usuario, saturar el servidor y desperdiciar ancho de banda valioso. ¿Te ha pasado que un usuario escribe rápidamente en un buscador y disparas una petición por cada letra? ¿O que navega de una vista a otra dejando peticiones pesadas ejecutándose en segundo plano?

En este tutorial aprenderás a dominar dos herramientas nativas de JavaScript sumamente potentes: AbortController y las estrategias de caché en el cliente. No necesitaremos librerías externas ni dependencias pesadas; utilizaremos la API Fetch estándar que ya viene integrada en los navegadores modernos.


1. Entendiendo el Problema de las Peticiones Redundantes

Imaginemos una interfaz de autocompletado típica. A medida que el usuario teclea el término "javascript", se generan múltiples eventos input:

  1. j -> Petición HTTP iniciada
  2. ja -> Petición HTTP iniciada
  3. jav -> Petición HTTP iniciada ...y así sucesivamente.

El problema crítico es que la red es asíncrona e impredecible. La petición para la letra j podría tardar más en responder que la petición para "javascript". Esto provoca condiciones de carrera (race conditions) donde resultados obsoletos sobrescriben información más reciente en pantalla, además de consumir recursos de red innecesariamente.

Condición de Carrera en Peticiones HTTP CLIENTE SERVIDOR 1. Petición A (Lenta) 2. Petición B (Rápida) Procesando... Estado: Datos B (¡Correcto por ahora!) Estado: Datos A ⚠️ DATOS OBSOLETOS RACE CONDITION B finaliza antes que A Respuesta A sobreescribe a B

Para solucionar esto, necesitamos dos superpoderes:

  • Cancelar peticiones que ya no son relevantes.
  • Cachear resultados de peticiones idénticas para no repetir llamadas innecesarias.
💡 Consejo: Cancelar peticiones HTTP no solo mejora la experiencia de usuario, sino que ahorra datos móviles en dispositivos con conexiones limitadas.

2. Dominando AbortController: Cancelando Peticiones HTTP

El AbortController es una interfaz estándar que te permite abortar una o más solicitudes web según sea necesario. Consta de dos partes principales:

  1. La clase AbortController() que crea un nuevo controlador.
  2. La propiedad signal de ese controlador, que se pasa como opción a la función fetch().

Sintaxis Básica y Primer Ejemplo

Veamos cómo crear un controlador y abortar una petición después de un tiempo determinado (timeout) o por una acción del usuario.

const controller = new AbortController();
const { signal } = controller;

async function fetchDataWithTimeout() {
  try {
    const response = await fetch('https://api.example.com/data', { signal });
    const data = await response.json();
    console.log('Datos recibidos:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.op('La petición fue cancelada exitosamente.');
    } else {
      console.error('Ocurrió un error de red:', error);
    }
  }
}

// Iniciamos la petición
fetchDataWithTimeout();

// Simulamos que el usuario cancela la acción a los 2 segundos
setTimeout(() => {
  controller.abort();
}, 2000);
⚠️ Advertencia: Cuando una petición es abortada mediante `AbortController`, la promesa devuelta por `fetch` se rechaza con un error cuyo nombre es AbortError. Es obligatorio capturar este error con un bloque try...catch para evitar excepciones no controladas en tu consola.

Implementando un Autocompletable Inteligente con AbortController

Apliquemos esto a un escenario real de búsqueda en tiempo real. Cada vez que el usuario escriba una nueva letra, cancelaremos la petición anterior si aún sigue activa.

let currentController = null;

async function searchProducts(query) {
  if (!query.trim()) return;

  // Si ya existe una petición en curso, la cancelamos
  if (currentController) {
    currentController.abort();
  }

  // Creamos un nuevo controlador para la nueva búsqueda
  currentController = new AbortController();
  const { signal } = currentController;

  try {
    const response = await fetch(`https://api.tienda.com/search?q=${encodeURIComponent(query)}`, { signal });
    
    if (!response.ok) {
      throw new Error('Error en el servidor');
    }

    const results = await response.json();
    updateUI(results);

  } catch (error) {
    if (error.name === 'AbortError') {
      console.log(`Búsqueda anterior cancelada para: "${query}"`);
    } else {
      console.error('Error al buscar:', error);
    }
  }
}

// Simulamos eventos de escritura rápida
searchProducts('j');
searchProducts('ja');
searchProducts('javascript');

3. Estrategias de Caché en JavaScript para Optimizar Red

Cancelar peticiones evita trabajo innecesario, pero ¿qué pasa si el usuario busca exactamente lo mismo dos veces? Volver a consultar al servidor es redundante si los datos no han cambiado. Aquí es donde entra en juego la gestión de caché.

Podemos implementar un sistema de caché ligero utilizando un objeto literal o un Map en JavaScript puro.

Caché en Memoria con un Objeto Simple

const apiCache = new Map();

async function cachedFetch(url) {
  // Verificamos si los datos ya existen en caché
  if (apiCache.has(url)) {
    console.log(`[Caché HIT]: Sirviendo datos desde memoria para ${url}`);
    return apiCache.get(url);
  }

  console.log(`[Caché MISS]: Realizando petición HTTP para ${url}`);
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error('Error al obtener los datos');
  }

  const data = await response.json();

  // Guardamos el resultado en caché antes de retornarlo
  apiCache.set(url, data);

  return data;
}

// Primera llamada: Realiza petición HTTP
await cachedFetch('https://api.example.com/usuarios');

// Segunda llamada inmediata: Utiliza caché en memoria
await cachedFetch('https://api.example.com/usuarios');
Petición Sin Caché (Lenta) Cliente Servidor Base Datos Petición Consulta Latencia Alta Petición Con Caché (Instantánea) Cliente Caché Local Base Datos Hit! Respuesta Inmediata No se consulta Ahorro de recursos y reducción de carga en el servidor

Añadiendo Tiempo de Expiración (TTL - Time To Live)

Un problema común de guardar datos en memoria indefinidamente es que pueden volverse obsoletos (stale data). Para solucionar esto, podemos asociar un tiempo de vida (TTL) a cada entrada del caché.

const advancedCache = new Map();
const DEFAULT_TTL = 60000; // 1 minuto en milisegundos

async function fetchWithTTL(url, ttl = DEFAULT_TTL) {
  const now = Date.now();
  const cachedItem = advancedCache.get(url);

  if (cachedItem && (now - cachedItem.timestamp < ttl)) {
    console.log('Sirviendo desde caché válido (TTL activo)');
    return cachedItem.data;
  }

  console.log('Caché expirado o inexistente. Consultando API...');
  const response = await fetch(url);
  const data = await response.json();

  // Guardamos los datos junto con la marca de tiempo actual
  advancedCache.set(url, {
    data: data,
    timestamp: now
  });

  return data;
}
📌 Nota: El uso de Map es preferible a los objetos planos ({}) en JavaScript cuando realizas inserciones y eliminaciones frecuentes de claves debido a su optimización interna en el motor V8.

4. Combinando AbortController y Caché

En aplicaciones de nivel de producción, lo ideal es combinar ambas técnicas. Imaginemos una función unificada que primero valida si hay caché disponible, y si no, ejecuta un fetch controlable mediante AbortController.

class SmartAPIClient {
  constructor() {
    this.cache = new Map();
    this.activeControllers = new Map();
  }

  async request(endpoint, options = {}) {
    const cacheKey = endpoint;
    const ttl = options.ttl || 30000;
    const now = Date.now();

    // 1. Verificar Caché
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey);
      if (now - cached.timestamp < ttl) {
        console.log(`⚡ Datos desde caché para: ${endpoint}`);
        return cached.data;
      }
    }

    // 2. Gestionar AbortController previo para este endpoint
    if (this.activeControllers.has(endpoint)) {
      this.activeControllers.get(endpoint).abort();
    }

    const controller = new AbortController();
    this.activeControllers.set(endpoint, controller);

    try {
      console.log(`🌐 Petición HTTP real para: ${endpoint}`);
      const response = await fetch(endpoint, { signal: controller.signal });
      
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      
      const data = await response.json();

      // Guardar en caché
      this.cache.set(endpoint, { data, timestamp: Date.now() });
      
      // Limpiar controlador activo
      this.activeControllers.delete(endpoint);

      return data;
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log(`❌ Petición cancelada: ${endpoint}`);
      } else {
        throw error;
      }
    }
  }
}

// Uso del cliente inteligente
const api = new SmartAPIClient();

// Primera petición
api.request('https://jsonplaceholder.typicode.com/todos/1');

5. Buenas Prácticas y Consejos de Rendimiento

Para cerrar con broche de oro, aquí tienes una tabla resumen con las mejores prácticas al consumir APIs utilizando estas técnicas:

PrácticaDescripciónBeneficio Principal
---------
Abortar en DesmontajeCancela peticiones pendientes si un componente de UI se desmonta.Evita fugas de memoria y errores de actualización de estado en elementos eliminados.
TTL AdecuadoConfigura tiempos de expiración lógicos según la volatilidad de los datos.Garantiza que el usuario no vea información desactualizada.
---------
Claves de Caché ÚnicasAsegúrate de incluir parámetros de consulta (query params) en la clave del caché.Evita colisiones donde una búsqueda distinta devuelva resultados incorrectos.
Manejo de Errores EspecíficoDistingue siempre entre un error de red real y un AbortError.Evita mostrar notificaciones de error molestas al usuario cuando él mismo canceló la acción.
¿Preguntas Frecuentes sobre AbortController y Caché?

P: ¿Puedo usar AbortController con librerías como Axios?
R: ¡Sí! Axios soporta nativamente AbortController a partir de la versión 0.22.0 utilizando la misma propiedad signal.

P: ¿Qué pasa si la memoria del navegador se llena con el Map de caché?
R: En aplicaciones grandes donde almacenes cientos de respuestas, es recomendable implementar una estrategia de reemplazo como LRU (Least Recently Used) o usar límites de tamaño máximo en tu caché.


Conclusión

El dominio de AbortController y las estrategias de caché en JavaScript te elevan a un nivel superior como desarrollador frontend. Ya no dependes exclusivamente de librerías de terceros para optimizar el tráfico de red de tus aplicaciones. Implementando estos patrones lograrás interfaces mucho más fluidas, con menor consumo de datos y una experiencia de usuario impecable. ¡Es hora de aplicarlo en tu próximo proyecto web! 🚀

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!