Volver al blog

Técnico optimización caché offline-first

¿Cómo diseñas software cuando Internet puede desaparecer en cualquier momento?

Por Armando Peña 8 min de lectura 6 de septiembre de 2026

graphs of performance analytics on a laptop screen

Foto de  Luke Chesser  en  Unsplash

La mayoría de los desarrolladores que trabajan para mercados con buena conectividad parten de una suposición bastante cómoda:

Internet funciona.

El usuario abre la aplicación, hace una petición, el servidor responde y continuamos.

Si tarda 300 ms, optimizamos.

Si tarda 2 segundos, nos preocupamos.

Si tarda 5 segundos, abrimos el panel de rendimiento y empezamos a buscar culpables.

Pero ¿qué ocurre cuando la conexión funciona a 120 kbps, se cae, vuelve a aparecer y vuelve a caerse?

👀

Entonces cada petición HTTP empieza a parecer una apuesta.

Y no, esto no es una exageración especialmente creativa.

Es una condición de desarrollo bastante real en Cuba.


🇨🇺 El desarrollador cubano no puede asumir que Internet funciona

Para un desarrollador internacional, una conexión a Internet suele ser una dependencia, no perfecta, pero confiable.

Puede diseñar pensando:

usuario
   ↓
Internet
   ↓
servidor

Y preocuparse después por qué hacer si algo falla.

En Cuba, esa suposición es fatal.

Cuando hay corriente, una conexión móvil puede ser extremadamente lenta o inestable. Y cuando no hay corriente...

Bueno.

El problema de rendimiento deja de ser exclusivamente un problema de software.

Los datos internacionales ayudan a ponerlo en perspectiva. Los datos de Ookla publicados por DataReportal situaban la mediana de descarga de las conexiones fijas cubanas en 3,48 Mbps, frente a 104,43 Mbps a nivel mundial.

Y eso es una mediana.

Un desarrollador cubano que esté intentando trabajar con una conexión real de 120 kbps no necesita que una estadística internacional le explique qué significa una conexión lenta.

Ya lo sabe.


🧠 La pregunta correcta no es "¿cómo hago mi aplicación más rápida?"

Es:

¿Cuánto trabajo necesita realmente mi aplicación para hacer lo que el usuario quiere?

Porque existe una optimización mucho mejor que hacer una operación más rápida:

No hacerla.

Si una imagen no necesita descargarse:

no la descargues.

Si un resultado ya está calculado:

no lo calcules otra vez.

Si los datos todavía son válidos:

no vuelvas a pedirlos.

Si el usuario no está viendo 9.000 elementos:

no renderices 9.000 elementos.

Si el servidor no necesita recibir un dato:

no lo envíes.

Esto parece obvio.

Sin embargo, muchas aplicaciones hacen exactamente lo contrario.


🌐 1. La red también es un recurso

Cada petición tiene un costo.

No solamente en bytes.

También implica:

  • resolución de nombres;
  • establecimiento de conexión;
  • cifrado;
  • latencia;
  • procesamiento en el servidor;
  • serialización;
  • análisis de la respuesta;
  • batería;
  • tiempo del usuario.

Por eso una API no debería devolver:

{
  "id": 123,
  "name": "Producto",
  "description": "...",
  "price": 500,
  "stock": 10,
  "images": [],
  "supplier": {},
  "reviews": [],
  "metadata": {}
}

si el componente solamente necesita:

{
  "id": 123,
  "name": "Producto",
  "price": 500
}

Los datos que nunca se envían son mejores que los datos comprimidos.

La compresión ayuda.

Eliminar trabajo ayuda más.


🖼️ 2. Una imagen también es información

Una imagen no debería tratarse como algo mágico que simplemente "está ahí".

Es información que alguien tiene que transferir.

Supongamos que tenemos una fotografía de:

2400 × 1800 px

pero nuestro componente solamente necesita:

300 × 225 px

Enviar la fotografía completa para reducirla después en el navegador significa transportar información que nunca necesitábamos.

Una estrategia más razonable es generar diferentes versiones:

imagen original
      │
      ├── 160 px → miniatura
      ├── 400 px → tarjeta
      ├── 800 px → detalle
      └── 1600 px → original

Y seleccionar la apropiada.

<img
  src="producto-400.webp"
  srcset="
    producto-400.webp 400w,
    producto-800.webp 800w
  "
  sizes="(max-width: 600px) 100vw, 400px"
  alt="Producto"
/>

Además podemos utilizar WebP, AVIF, compresión y carga diferida.

Pero hay una diferencia importante:

La carga diferida no hace que una imagen pese menos.

Simplemente evita descargarla antes de que sea necesaria.

Una técnica reduce el tamaño.

La otra reduce el trabajo realizado.

Son problemas diferentes.


⚙️ 3. Optimizar código también significa ejecutar menos código

Cuando pensamos en optimización solemos imaginar:

algoritmo A → 100 ms
algoritmo B → 50 ms

Pero existe una optimización todavía mejor:

algoritmo → no ejecutar

Algunos ejemplos.

⌨️ Debounce (desaceleración controlada)

Si el usuario escribe:

c
ca
cam
cama

no necesariamente necesitamos hacer cuatro peticiones.

Podemos esperar unos milisegundos después de que deje de escribir:

c
ca
cam
cama
   ↓
 esperar
   ↓
petición

🧠 Memoization (memorización)

Si una operación costosa recibe los mismos parámetros:

f(x) → resultado

podemos guardar el resultado y evitar repetir el cálculo.

📋 Virtualization (virtualización)

Si tenemos 10.000 elementos pero solamente 20 son visibles:

❌ renderizar 10.000

no tiene demasiado sentido.

Podemos mantener en el DOM solamente una pequeña ventana:

        10.000 elementos
              │
              ▼
      ┌──────────────┐
      │  visibles 20 │
      └──────────────┘

El usuario sigue percibiendo una lista enorme.

El navegador no tiene que trabajar como si estuviera mostrando todos los elementos simultáneamente.


🧠 4. ¿Por qué volver a pedir información que ya tenemos?

Aquí aparece una de las herramientas más importantes para reducir trabajo:

Caché

Una implementación sencilla puede seguir este patrón:

Petición
   ↓
¿Está en caché?
   │
   ├── Sí → devolver
   │
   └── No
        ↓
       API
        ↓
      Caché
        ↓
      devolver

Esto no solamente reduce la latencia.

Reduce peticiones.

Reduce carga del servidor.

Y, especialmente importante bajo conectividad limitada, reduce nuestra dependencia de la red.


📦 Una caché necesita límites

Una caché infinita sería bastante fácil:

guardar todo

Pero la memoria tampoco es infinita.

Por eso existen estrategias como LRU (menos recientemente utilizado).

La idea es sencilla:

┌───────────────────────┐
│ producto:123          │ ← utilizado recientemente
│ producto:456          │
│ producto:789          │
│ producto:321          │
│ producto:999          │ ← menos recientemente utilizado
└───────────────────────┘

Cuando necesitamos espacio:

sale el elemento que lleva más tiempo sin utilizarse.

Es una estrategia especialmente útil cuando los datos tienen patrones de reutilización.

De hecho, implementé una ligera biblioteca con este mecanismo: adev-lru.

La idea importante no es la biblioteca.

Es la decisión arquitectónica:

Tenemos recursos limitados. ¿Qué información merece permanecer disponible localmente?


⚠️ Pero una caché también puede mentir

Supongamos:

BASE DE DATOS
precio = $550

pero:

CACHÉ
precio = $500

Ahora tenemos dos versiones de la realidad.

La caché puede ser:

  • rápida;
  • local;
  • disponible;

y al mismo tiempo estar desactualizada.

Por eso aparecen conceptos como:

  • TTL (tiempo de vida);
  • invalidación;
  • stale-while-revalidate (servir datos antiguos mientras se actualizan);
  • ETag;
  • versionado;
  • cache-aside (caché bajo demanda).

Una estrategia puede ser:

Caché
  ↓
mostrar inmediatamente
  ↓
consultar servidor
  ↓
actualizar caché

El usuario recibe una respuesta rápida.

La aplicación obtiene una versión actualizada cuando puede.

Offline-first (prioridad al funcionamiento sin conexión) no significa offline forever.

Significa reducir cuánto dependemos de que el servidor responda inmediatamente.


🔄 5. ¿Y si necesitamos escribir datos?

Leer desde una caché es relativamente sencillo.

Escribir es donde empiezan los problemas interesantes.

Imaginemos que el usuario está sin conexión y realiza tres operaciones:

➕ crear pedido
✏️ modificar dirección
🗑️ eliminar producto

No podemos enviarlas inmediatamente.

Pero tampoco queremos perderlas.

Podemos mantener una cola local:

SIN CONEXIÓN

crear pedido
     ↓
modificar dirección
     ↓
eliminar producto
     ↓
┌─────────────────┐
│ cola de         │
│ operaciones     │
└─────────────────┘

Cuando vuelve la conexión:

CON CONEXIÓN
     ↓
cola
     ↓
sincronización
     ↓
API

Esto transforma el problema.

La pregunta deja de ser:

"¿Cómo hago para que Internet nunca falle?"

Y pasa a ser:

"¿Cómo hago para que mi aplicación pueda sobrevivir cuando falle?"


🔁 6. Los reintentos tampoco son tan sencillos

Supongamos:

POST /orders

El servidor recibe correctamente el pedido.

Pero justo después se pierde la conexión.

El cliente nunca recibe:

200 OK

Entonces piensa:

"Falló."

Y vuelve a enviar la petición.

Ahora podríamos tener:

Pedido #123
Pedido #124

aunque el usuario solamente quería uno.

Por eso los sistemas que realizan operaciones importantes necesitan pensar en idempotencia.

Una solución habitual es utilizar una clave de idempotencia:

Idempotency-Key: 8f2a...

El servidor puede reconocer que dos peticiones representan la misma operación.

Esto es especialmente importante cuando diseñamos sistemas que tienen que tolerar:

  • reintentos;
  • tiempos de espera;
  • desconexiones;
  • peticiones duplicadas.

La red puede fallar.

Nuestra aplicación no debería interpretar cada fallo de red como un fallo de la operación.


⚔️ 7. ¿Qué pasa si dos dispositivos modifican lo mismo?

Ahora tenemos un problema todavía más interesante.

              SERVIDOR
                 │
        ┌────────┴────────┐
        │                 │
   Dispositivo A     Dispositivo B
        │                 │
    sin conexión       sin conexión
        │                 │
    precio = 500       precio = 550
        │                 │
        └────────┬────────┘
                 │
              conexión

¿Quién tiene razón?

Aquí aparecen problemas de:

  • resolución de conflictos;
  • versionado;
  • concurrencia optimista;
  • marcas de tiempo;
  • último escrito gana;
  • consistencia eventual.

Ya no estamos hablando simplemente de "hacer una web rápida".

Estamos diseñando un sistema distribuido.

Y todo comenzó porque alguien perdió la conexión.


🇨🇺 Diseñar para restricciones

Hay mercados donde un desarrollador puede asumir razonablemente que una conexión permanente está disponible.

Y hay otros donde esa suposición simplemente no tiene sentido.

Cuba es un ejemplo extremo.

Cuando la conexión disponible puede rondar los 120 kbps y además ser inestable, optimizar una aplicación deja de ser una cuestión estética.

Es una cuestión de funcionamiento.

Una aplicación que hace diez peticiones para mostrar información que podría haber obtenido en una sola está desperdiciando recursos.

Una aplicación que descarga una imagen de varios megabytes para mostrar una miniatura está desperdiciando recursos.

Una aplicación que pierde todo su estado cuando desaparece la conexión está delegando una parte importante de su funcionamiento a una infraestructura que el usuario no controla.

Y esto no es exclusivamente cubano.

Los mismos principios aparecen en:

  • aplicaciones móviles;
  • zonas rurales;
  • sistemas de campo;
  • dispositivos IoT;
  • transporte;
  • aplicaciones que deben funcionar durante viajes;
  • sistemas distribuidos con conectividad intermitente.

La diferencia es que en algunos lugares estas situaciones son excepcionales.

En Cuba pueden formar parte del día a día.


🚧 La optimización empieza en la arquitectura

Durante mucho tiempo hemos tratado la optimización como una etapa posterior:

1. construir
2. publicar
3. medir
4. optimizar

Pero cuando existen restricciones fuertes, algunas decisiones tienen que aparecer mucho antes.

La arquitectura determina:

  • cuánto dependemos del servidor;
  • cuánto descargamos;
  • cuánto procesamos;
  • cuánto almacenamos;
  • qué ocurre cuando falla una conexión;
  • cuánto puede seguir haciendo el usuario sin conexión.

Una aplicación bien diseñada para estas condiciones no necesariamente es una aplicación "más lenta".

Puede ser exactamente lo contrario.

Puede hacer menos trabajo, transferir menos información, realizar menos peticiones y continuar funcionando cuando otras aplicaciones simplemente muestran un error.


💡 La idea final

Internet no es una constante.

Es una dependencia.

Y como cualquier dependencia, podemos reducir cuánto necesitamos de ella.

Guardar información en caché.

Comprimir imágenes.

Enviar solamente los datos necesarios.

Evitar cálculos repetidos.

Virtualizar grandes cantidades de elementos.

Guardar operaciones localmente.

Sincronizar posteriormente.

Hacer las peticiones idempotentes.

Resolver conflictos.

Ninguna de estas técnicas es especialmente nueva.

Lo interesante aparece cuando las combinamos.

Porque entonces dejamos de intentar construir una aplicación que funcione solamente cuando todo funciona bien.

Construimos una aplicación que pueda seguir haciendo su trabajo cuando algunas cosas fallen.

Y quizá esa sea una de las mejores definiciones de una buena arquitectura:

No aquella que funciona perfectamente en condiciones perfectas, sino aquella que sigue siendo útil cuando las condiciones dejan de serlo.

¿Te ayudó este artículo?

Si necesitas ayuda con tu presencia digital, cuéntanos tu idea.

Consultar por WhatsApp

Posts similares

person writing on white board
  • low-net
  • low-bandwidth
  • offline-first

Cómo transmitir una pizarra con 120 Kbps y una conexión inestable?

Quería que un dibujo cruzara la conexión de mi clase de inglés, alrededor de 120 kbps y con caídas constantes. Empecé comprimiendo una pizarra y descubrí que comprimir era la parte fácil: el problema era decidir qué información enviar y qué hacer cuando la conexión simplemente no está. Esta es la cadena de descubrimientos que llevó a LOW-NET, el laboratorio donde estoy midiendo esa pregunta.

Caso de estudio1940 palabras · 10 min de lectura

Armando Peña 1 0
a person holding a cell phone in their hand
  • páginas web
  • internet en Cuba
  • conexión lenta

Con lo mala que está la conexión, ¿para qué quiero una página web?

¿Realmente tiene sentido una página web cuando la conexión a Internet en Cuba es tan limitada? Comparamos WhatsApp, PDF y páginas web reales para descubrir cuánto cuesta descargar un catálogo y qué significa construir una web pensando en conexiones lentas.

Negocios1141 palabras · 6 min de lectura

Armando Peña 3 0
man in black long sleeve shirt using computer
  • atribucion
  • utm
  • analytics

Como medir marketing cuando las redes no te dan datos? La atribución va en el enlace!

Las plataformas no te dicen de qué publicación salió tu cliente, y no tienen por qué. La atribución la pones tú, en el enlace, antes de publicar. Este es el sistema mínimo y honesto para saber qué canal funciona, de las visitas a las ventas: empieza por UTM y añade complejidad solo cuando hay un problema real.

Técnico1539 palabras · 8 min de lectura

Armando Peña 1 0