Lección 05

¿Por qué cuatro peticiones se sienten como ocho veces más lentas?

Cuando cada petición necesita la respuesta de la anterior, los tiempos se suman. Se ataca en tres frentes: pedir a la vez, pedir en bloque y no volver a pedir.

01Qué es una cascada

Una petición HTTP es un viaje: el navegador pregunta y el servidor contesta. Aunque la respuesta sea diminuta, cuesta una ida y vuelta (RTT: lo que tarda un mensaje en ir y volver), del orden de 50-200 ms en red móvil. No depende del tamaño del cuerpo, sino de la distancia y de las vueltas.

Una cascada (waterfall) es una secuencia en la que cada petición espera a que termine la anterior. En Network se ve como una escalera: barras que empiezan donde termina la de arriba. Esa escalera es la firma visual de una dependencia.

En serie, cada barra empieza donde acaba la anterior en serie: cuatro viajes en paralelo: una espera perfil pedidos saldo avisos 4 × 180 ms ≈ 720 ms 1 × 180 ms la red es la misma; cambia cuántas veces esperas
La escalera no es un problema de velocidad, sino de forma: los mismos datos, en otro orden, cuestan una cuarta parte.

02Por qué sucede: los tiempos se suman

Piénsalo como mandar a un mensajero. Le pides un tornillo; vuelve, y descubres que faltaba el taquete; lo mandas otra vez; luego, la broca. Cuatro recados encadenados son cuatro viajes. Con cuatro mensajeros a la vez, es uno. Y con la lista completa en una sola nota, también.

La pantalla pide el perfil y, con su id, pedidos, saldo y avisos. Encadenado son cuatro vueltas de 180 ms: unos 720 ms antes de pintar algo. Si las tres últimas solo necesitan un id que ya tienes, pueden salir juntas: 180 ms más pintar.

Un endpoint que agrupa gana incluso a cuatro peticiones perfectas, porque lo que se paga no es el byte: es la vuelta. Cuatro viajes son cuatro colas, cuatro cabeceras y cuatro claves que invalidar; uno solo es una vuelta. Por eso la misma pantalla se siente ocho veces más lenta sin que ninguna petición lo sea: la percepción no suma milisegundos, suma esperas.

03Cómo se ataca: paralelo, bloque y caché

Paralelo. Si dos peticiones no dependen una de otra, no hay razón para esperar: Promise.all las lanza juntas. El navegador abre varias conexiones por dominio, así que el reloj corre una vez. Sirve para lo independiente: no se puede paralelizar lo que necesita un dato que no llegó.

Bloque. Un endpoint que devuelve la pantalla completa convierte cuatro viajes en uno: cuesta un join en el backend y suele ser el arreglo de fondo.

Caché HTTP. El navegador guarda la respuesta si el servidor lo permite: Cache-Control: max-age=60 significa "vale un minuto, no preguntes". Pasado el minuto revalida con ETag y el servidor contesta "no cambió" (304): otra vuelta, pero sin cuerpo.

Caché de aplicación. Aquí guardas tú los datos. El patrón stale-while-revalidate sirve la copia al instante y pide la nueva en segundo plano: el usuario ve algo ya y la pantalla se corrige sola. Es la nota con el teléfono en el corcho: respondes sin mensajero, pero si el número cambió, la nota miente.

El problema difícil es la invalidación: una caché es una promesa sobre cuándo cambian los datos, y eso casi nadie lo sabe. Un TTL es una apuesta; invalidar por evento es exacto pero necesita un canal que avise. Si guardas y no invalidas, el cambio del usuario desaparece al recargar.

El patrón N+1 es la cascada con disfraz: una petición para la lista y una por elemento. Con 30 filas son 31 vueltas, y crecen con los datos. Se detecta contando peticiones por pantalla, no por su lentitud. Se arregla en el servidor con un join o un lote por ids; en el cliente, pidiendo el detalle al abrir la fila.

// En serie: cada await espera a la anterior. Los tiempos se suman.
const get = url => fetch(url).then(r => r.json());

const perfil  = await get('/api/perfil');
const pedidos = await get(`/api/pedidos?u=${perfil.id}`);
const saldo   = await get(`/api/saldo?u=${perfil.id}`);
const avisos  = await get(`/api/avisos?u=${perfil.id}`);
// ~4 × 180 ms ≈ 720 ms antes de pintar nada

// En paralelo: solo si las tres son independientes del perfil.
const [p, s, a] = await Promise.all([
  get('/api/perfil'),
  get('/api/saldo'),
  get('/api/avisos'),
]);
// ~180 ms, pero siguen siendo tres viajes y tres fallos

// Agrupado: un viaje y un contrato. Lo pide el frontend,
// lo sirve el backend.
const resumen = await get('/api/resumen?u=42');
// ~180 ms: una petición que cachear, una clave que invalidar
La suma en una línea de tiempo serie 180 ms 180 ms 180 ms 180 ms total: 720 ms paralelo total: 180 ms 0 180 360 540 720
Cuatro barras encadenadas ocupan el ancho entero; las mismas superpuestas caben en el primer tramo. El eje es tiempo, no datos.
Stale-while-revalidate: responde ya y corrige después pantalla copia vieja visible caché copia guardada servidor dato al día 0 ms revalida dato nuevo se corrige sola al llegar
El usuario ve algo de inmediato porque la caché contesta primero; la verdad llega después y reemplaza la copia. El trato solo funciona si además invalidas al escribir.
Cómo se pide al backend

Con tres datos, no con una opinión: cuántas peticiones hace la pantalla, cuánto es ida y vuelta y qué forma tendría el endpoint que lo sustituye. "Hace 31 peticiones, 30 son una por fila" abre la conversación; "va lento" no.

04Cómo diagnosticarlo

En Network, la forma importa más que la lista de tiempos.

HerramientaQué se miraCómo se ve el síntoma
Network › cascada Si se solapan o van en escalón. Cuatro barras en escalera: ninguna empieza antes de que acabe la anterior.
Network › Queueing y Stalled El tiempo gris antes de enviar, separado del de respuesta. Queueing alto: espera a que se libere una conexión. Stalled: prioridad o socket ocupado.
Network › TTFB El tiempo hasta el primer byte. TTFB alto y descarga diminuta: el problema es la vuelta, no el tamaño.
Network › n.º de peticiones Peticiones al cargar la pantalla. 31 donde deberían ser 2 o 3: la forma del N+1.
Para decirlo en la entrevista

“Serial requests add up: each one waits for the previous response, so latency multiplies by the number of steps.”

“I parallelize independent calls with Promise.all, but the real win is one endpoint that returns the whole screen.”

“I cache with stale-while-revalidate and treat invalidation as the hard part: a cache key is a promise about when data changes.”

Comprueba que lo tienes

Cuatro preguntas. Al comprobar se marca la lección como completada.

Cuatro peticiones de 180 ms encadenadas tardan ~720 ms. ¿Por qué el total no es 180 ms?

Promise.all sobre cuatro peticiones independientes tarda ~180 ms. ¿Qué NO resuelve?

El usuario guarda un cambio y ve datos viejos durante un segundo. Con stale-while-revalidate, ¿qué está pasando?

En Network ves una petición de lista y 30 pequeñas, una por fila. ¿Qué es y qué se pide?