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.
// 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.
// 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.
// 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.
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.
// 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.
// Dependencia REAL: el cliente sale del pedido, no hay como paralelizarconst pedido = await getPedido(pedidoId);const cliente = await getCliente(pedido.clienteId); // necesita pedidoLa 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.