Saltar al contenido

Waterfalls: detectarlos y disparar en paralelo

El anti-patrón que arruina la latencia y cómo erradicarlo. Qué es un waterfall de peticiones —cargas dependientes encadenadas en serie—, cómo detectarlo por su forma de escalera en la pestaña de red, cómo el `preload` de la ruta dispara todas las queries en paralelo antes de renderizar y cómo `query` deduplica para que las lecturas anidadas reusen lo ya en vuelo, cuándo `Promise.all` resuelve dependencias falsas, y por qué una dependencia real se minimiza pero no se elimina.

Qué es un waterfall

Hay dos formas canónicas. La primera es el waterfall de datos: dos peticiones independientes que se esperan porque las escribiste en serie. La segunda es el waterfall de código y datos: la ruta espera a descargar su JavaScript antes de empezar a pedir. En ambos casos, tiempos que podían solaparse se suman.

TSX
// WATERFALL de datos: posts NO depende de perfil, pero lo esperaasync function cargar(id: string) {  const perfil = await getPerfil(id);   // primero esto...  const posts = await getPosts(id);      // ...y solo despues esto  return { perfil, posts };              // latencia = perfil + posts}

Nada obliga a que posts espere a perfil: ambos dependen solo de id, que ya tienes. La estructura del código —dos await seguidos— ha inventado una dependencia que no existe, y el usuario paga la suma de dos latencias donde debería pagar la mayor de las dos.

Detectarlos: la forma de escalera

Un waterfall tiene una firma visual inconfundible en la pestaña de red: las barras de las peticiones se dibujan escalonadas, cada una empezando donde la anterior terminó, como peldaños que bajan hacia la derecha. Peticiones paralelas, en cambio, arrancan alineadas a la izquierda y se solapan en el tiempo.

TSX
// Escalera (malo):        Paralelo (bueno):// perfil  [====]          perfil  [====]// posts        [====]     posts   [====]// avatar            [==]  avatar  [==]

La regla de lectura es directa: si ves peldaños y las peticiones de abajo no dependían de las de arriba, has encontrado un waterfall accidental. El objetivo es aplanar esos peldaños hasta alinearlos a la izquierda.

Disparar en paralelo desde la ruta

La palanca estructural es el preload de la ruta. Ahí invocas todas las queries que la pantalla necesitará sin esperarlas: al no haber await, salen todas a la vez, en paralelo, en cuanto hay intención de navegar. Los componentes las leen luego con createAsync, y query garantiza que esa lectura recoja la petición ya en vuelo en vez de lanzar una nueva.

TSX
// routes/usuarios/[id].tsxexport const route = {  preload: ({ params }) => {    getPerfil(params.id);  // se disparan    getPosts(params.id);   // las dos    getAvatar(params.id);  // a la vez  },} satisfies RouteDefinition;

Este es el porqué de que query y preload no fueran dos temas sueltos sino uno solo. La deduplicación por clave es lo que permite disparar en la ruta y leer en cualquier componente anidado sin duplicar: no importa a qué profundidad del árbol un componente pida getPosts(id), resolverá a la misma clave que el preload ya calentó. El paralelismo se declara arriba, en la ruta; la lectura se reparte abajo, en los componentes; y la clave los cose sin una sola petición de más.

Cuando la latencia manda dentro de una misma función de servidor, Promise.all expresa el paralelismo de forma explícita: lanzas las promesas y esperas a todas juntas, no una tras otra.

TSX
async function cargar(id: string) {  const [perfil, posts] = await Promise.all([getPerfil(id), getPosts(id)]);  return { perfil, posts }; // latencia = la mayor, no la suma}

Hay una tercera fuente de waterfalls, más sigilosa, en la colocación de los Suspense. Dos createAsync hermanos bajo un mismo límite de Suspense disparan sus peticiones a la vez y revelan juntos; pero si anidas un Suspense dentro de otro, el interior no empieza a montar —ni a pedir— hasta que el exterior resuelve, y has vuelto a serializar. La regla práctica es agrupar bajo un solo límite lo que puede cargar en paralelo, y anidar límites solo cuando de verdad quieras que una sección espere a otra.

TSX
// Paralelo: ambos createAsync corren bajo el MISMO Suspense<Suspense fallback={<Cargando />}>  <Perfil id={id} />   {/* getPerfil */}  <Posts id={id} />    {/* getPosts, a la vez que getPerfil */}</Suspense>

Cuando la dependencia es real

No todo encadenamiento es un pecado. A veces la segunda petición necesita el resultado de la primera: pides un pedido para conocer el id de su cliente, y solo entonces puedes pedir ese cliente. Esa dependencia es real y no se puede paralelizar —ningún truco adelanta un dato que aún no existe—. Lo que sí puedes es minimizar la cadena: prefetchar la primera petición en el preload para que su leg arranque durante la navegación, mantener la profundidad de la cadena tan corta como el dominio permita, y no colgar de ella peticiones que en realidad no dependían.

TSX
// Dependencia REAL: el cliente sale del pedido, no hay como paralelizarconst pedido = await getPedido(pedidoId);const cliente = await getCliente(pedido.clienteId); // necesita pedido

La disciplina, entonces, no es “nunca encadenes” sino “encadena solo lo que el dato obliga, y que cada eslabón se gane su lugar”. Un waterfall es aceptable exactamente en la medida en que refleja una dependencia genuina; deja de serlo en cuanto encadena cosas que podían haber volado juntas.