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.