Cómo eliminar las solicitudes de cascadas de agua en SvelteKit con funciones de carga paralela
Aprenda por qué los await secuenciales ralentizan las páginas, y cómo SvelteKit carga funciones, Promise.all, llamadas cuidadosas a parent() y promesas en flujo eliminan los efectos en cascada.
Una página puede parecer lenta en una conexión rápida y con un backend veloz cuando sus solicitudes se ejecutan una tras otra en lugar de simultáneamente. Cada viaje de ida y vuelta espera a que termine el anterior, por lo que el tiempo necesario para mostrar el primer contenido relevante se convierte en la suma de todas las solicitudes en lugar del tiempo que tarda la más lenta. Esta guía explica de dónde provienen estos procesos en secuencia y muestra cómo estructurar las funciones load de SvelteKit para que los datos críticos lleguen en paralelo, el diseño y la carga de la página no se bloqueen mutuamente, y los datos lentos y no esenciales se envíen después de la primera renderización.
Así es un proceso en secuencia de solicitudes
Abra el panel de Red en las herramientas de desarrollador del navegador en un panel típico renderizado por el cliente con React o Vue y cargue una página que necesite varios datos. Las barras suelen formar una escalera: se completa la primera solicitud, solo entonces comienza la segunda, y la tercera espera a la segunda. Cada paso añade un viaje completo de red.
El código detrás de esa estructura en forma de escalera suele parecer inofensivo. Un componente se monta en el navegador y solo entonces descubre qué necesita. Dentro de un efecto o hook de montaje se realiza algo como const user = await getUser(), luego const posts = await getPosts(), y finalmente const comments = await getComments(). Tres líneas, tres llamadas a await; nada aparentemente incorrecto.
El problema radica en lo que significa await. Este suspende la función hasta que la promesa se resuelva, por lo que la solicitud de posts ni siquiera puede enviarse hasta que haya llegado por completo la respuesta de user, y comments espera a su vez a posts. Supongamos que cada llamada tarda alrededor de 300 ms, incluyendo el procesamiento del DNS, TLS y del servidor, así como la transferencia de datos. La página, entonces, no tiene nada útil que mostrar durante aproximadamente el tiempo total de las tres operaciones, y no durante el tiempo de la más larga.
Nada lanza errores, ninguna prueba falla y cada componente hace exactamente lo que se le indicó. Los usuarios simplemente ven un indicador de carga durante aproximadamente tres veces más tiempo del necesario. Esa invisibilidad es lo que hace que los modelos en cascada sean tan comunes.
Por qué los modelos en cascada siguen apareciendo
Tres hábitos comunes son la causa principal de ellos:
- Cada componente obtiene sus propios datos. En las aplicaciones renderizadas en el cliente, a menudo el componente padre debe terminar de cargarse antes de que el hijo pueda iniciarse, y el hijo no puede comenzar su propia solicitud hasta recibir una propiedad del padre. El árbol de componentes se convierte en una cadena de solicitudes.
- Esperar línea por línea por costumbre. Las instrucciones
awaitsecuenciales se leen de manera clara, y precisamente por eso ocultan el hecho de que serializan solicitudes que nunca dependieron unas de otras. - No se necesita una única vista de los datos de la página. Cuando el código de obtención de datos está distribuido en tres archivos, es difícil darse cuenta de que todas las solicitudes podrían haberse iniciado al mismo tiempo.
Escribir código más rápido no ayuda. Lo que sí funciona es mover la obtención de datos a un lugar y momento diferentes: antes de que exista ningún componente, en un único punto coordinado. En SvelteKit, ese punto es la función load. Para una visión independiente del framework sobre estas mismas opciones de concurrencia, consulte la comparación del blog sobre Promise.all, Promise.race y sequential awaits.
Cómo las funciones load de SvelteKit cambian la configuración por defecto
Cualquier ruta de SvelteKit puede exportar una función load que se ejecuta antes de que se renderice su componente de página. Los componentes ya no buscan datos después de montarse; la ruta recopila todo primero y lo pasa a la página como una única propiedad data.
Existen dos variantes, y la elección es importante:
- Carga universal en
+page.jsse ejecuta en el servidor durante la renderización del lado servidor y en el navegador durante la navegación del lado cliente. Es adecuada para llamadas a APIs públicas y para devolver valores que no son serializables. - Carga del servidor en
+page.server.jsse ejecuta únicamente en el servidor. Su código nunca se envía al navegador, por lo que puede utilizar sin problemas clientes de base de datos, variables de entorno privadas y tokens de autenticación.
Los layouts siguen el mismo patrón con +layout.js y +layout.server.js.
En la mayoría de las aplicaciones reales, todo lo que involucre una base de datos, un servicio interno o información confidencial debe estar en +page.server.js. Su forma mínima se ve así:
// src/routes/dashboard/+page.server.js
export async function load({ fetch }) {
const res = await fetch('/api/user');
const user = await res.json();
return {
user
};
}
Tres puntos sobre este fragmento merecen ser asimilados cuanto antes.
La función de carga finaliza antes de que se renderice la página
Para cuando +page.svelte comienza a renderizarse, user ya es un objeto simple. No existe ningún gancho de montaje, ni efecto de carga temporal, ni momento en el que su valor sea undefined.
Utilice el fetch que proporciona SvelteKit
El argumento fetch no es el global. SvelteKit ofrece una versión mejorada que acepta URLs relativas como /api/user, reenvía las cookies y encabezados de la solicitud entrante cuando se ejecuta durante el renderizado del lado servidor, y, cuando apunta a otra ruta dentro de la misma aplicación, llama directamente a ese manejador en lugar de realizar una solicitud HTTP real. Importar un fetch global pierde todo eso.
El valor devuelto se convierte en la propiedad de datos de la página
Lo que sea que devuelva la función se expone al componente de página como data. En un componente de Svelte 5 se lee con let { data } = $props(); (el código anterior de Svelte 4 utiliza export let data;) y luego se muestra data.user.name en el marcado.
El modelo mental es sencillo: primero obtener los datos, luego renderizarlos, y recibir valores ya resueltos en lugar de promesas que hay que gestionar dentro del componente.
No obstante, trasladar la obtención de datos a load por sí solo no elimina el enfoque secuencial. Tres llamadas a await consecutivas dentro de load recrean la misma secuencia en el servidor. La solución real viene a continuación.
Inicio simultáneo de todas las solicitudes independientes
La solución consiste en enviar inmediatamente todas las solicitudes independientes y esperarlas juntas:
// src/routes/dashboard/+page.server.js
export async function load({ fetch }) {
// Kick off all three requests immediately - none of them
// are awaited yet, so none of them block each other.
const userPromise = fetch('/api/user').then(r => r.json());
const postsPromise = fetch('/api/posts').then(r => r.json());
const commentsPromise = fetch('/api/comments').then(r => r.json());
// NOW wait for all of them to finish, in parallel.
const [user, posts, comments] = await Promise.all([
userPromise,
postsPromise,
commentsPromise
]);
return { user, posts, comments };
}
Por qué esto es más rápido
El factor decisivo es el intervalo entre enviar una solicitud y esperar su resultado. Al llamar a fetch(...) se envía la solicitud de inmediato; no espera a un await. Adjuntar .then() simplemente describe qué hacer con la respuesta cuando finalmente llegue. Así que en el código anterior:
- la primera línea envía la solicitud a
/api/user; - la segunda línea envía la solicitud a
/api/postsmientras la primera aún está en tránsito; - la tercera línea envía la solicitud a
/api/commentsmientras las otras dos aún están en tránsito; - solo
Promise.allrealmente se detiene, y reanuda su ejecución en cuanto finalice la más lenta de las tres solicitudes.
El tiempo total disminuye de la suma de tres solicitudes a aproximadamente la duración de la más lenta. Las solicitudes, el servidor y los datos permanecen sin cambios; solo se ha modificado la ubicación de await. En un escenario ilustrativo donde cada solicitud tarda unos 300 ms más algún tiempo adicional, eso hace que la página pase de ser utilizable en unos 960 ms a serlo en unos 360 ms, y la diferencia se amplía en redes lentas o con una API sobrecargada.
Pocos cambios en una página con mucho contenido ofrecen tanto beneficio con tan poco esfuerzo. No hay bibliotecas que añadir ni arquitecturas que rediseñar; solo se reordenan unas pocas líneas.
Qué tener en cuenta con Promise.all
Promise.all rechaza en cuanto alguna de sus promesas lo haga. Si una solicitud fallida a comments no debe hacer que toda la página deje de funcionar, utilice Promise.allSettled o asigne a cada promesa su propio .catch() que devuelva un valor de respaldo. Tenga también en cuenta que r.json() no verifica r.ok; una respuesta 404 o 500 con un cuerpo de error en JSON será analizada y devuelta como si fuera datos, por lo que verifique el estado cuando la corrección dependa de él.
La cascada oculta entre diseños y páginas
Los desarrolladores que han aprendido el truco de Promise.all a menudo siguen implementando otro tipo de cascada, una que abarca varios archivos: cómo la carga de datos de un diseño interactúa con la del página que se encuentra debajo de él.
Por defecto, SvelteKit inicia de forma concurrente las funciones load de +layout.server.js y +page.server.js, al igual que en el ejemplo paralelo anterior. La solución es parent(), que permite a la función load de una página acceder a los datos devueltos por los layouts superiores. A veces eso es exactamente lo que se necesita, por ejemplo cuando la consulta de la página requiere un ID de usuario que solo obtuvo el layout. Sin embargo, si se llama demasiado temprano, serializa todo lo que viene después:
// src/routes/dashboard/+page.server.js
// BAD: this creates a waterfall between the layout and the page,
// even if `posts` doesn't actually need anything from `parent()`.
export async function load({ parent, fetch }) {
const { user } = await parent(); // blocks here until layout's load finishes
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
return { posts };
}
Si la solicitud de publicaciones realmente necesita user.id, este orden es inevitable y está perfectamente bien; se trata de una dependencia real. El problema surge cuando la página también necesita datos que no tienen nada que ver con el diseño. Esperar a parent() en la primera línea retrasa todas las instrucciones posteriores, incluidas aquellas solicitudes independientes, hasta que finalice el diseño.
La solución es reordenar las acciones: comenzar primero con el trabajo independiente y esperar a parent() solo en el momento en que se necesite su valor.
// src/routes/dashboard/+page.server.js
// GOOD: independent work starts immediately; parent() is only
// awaited once we actually need the merged result.
export async function load({ parent, fetch }) {
const commentsPromise = fetch('/api/comments').then(r => r.json());
const { user } = await parent(); // runs concurrently with the fetch above
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
const comments = await commentsPromise;
return { user, posts, comments };
}
Aquí la solicitud de comentarios se envía antes de que la página espere al diseño, por lo que ambos procesos se superponen. La solicitud de publicaciones sigue teniendo que esperar a user.id, lo cual es correcto, y generalmente la promesa relacionada con los comentarios ya se ha resuelto para cuando se espera su resultado al final.
Regla práctica: trata await parent() como cualquier otro await. Colócalo exactamente donde se necesite el valor, nunca de forma automática al principio de la función, y ejecuta cualquier solicitud que no dependa de los datos del padre antes que él.
Transmisión en flujo de datos no críticos
Promise.all no siempre es la solución adecuada. Si una solicitud es lenta y sus datos no son necesarios para lo primero que ve el usuario, esperarla hace que toda la página sea tan lenta como su parte menos importante. En una página de producto, el nombre, el precio y las imágenes son críticos; la sección de reseñas, que está varias pantallas más abajo, no lo es.
Para ese caso, SvelteKit admite la transmisión en flujo. Devuelve una promesa desde un servidor load sin esperarla, y SvelteKit envía la página renderizada de inmediato, entregando luego el valor de la promesa al navegador una vez que se resuelve.
// src/routes/product/[id]/+page.server.js
export async function load({ fetch, params }) {
// Critical: awaited, blocks the initial render - but it's fast.
const product = await fetch(`/api/product/${params.id}`).then(r => r.json());
// Non-critical: NOT awaited. This is handed to the page as a
// pending Promise, and SvelteKit streams it in once it resolves.
const reviewsPromise = fetch(`/api/product/${params.id}/reviews`).then(r => r.json());
return {
product, // resolved value
reviews: reviewsPromise // still-pending promise
};
}
Se espera el producto porque la renderización inicial lo necesita y la solicitud es rápida. La solicitud de reseñas se inicia pero no se espera, por lo que la página recibe una promesa pendiente en data.reviews.
En la página, el bloque {#await} de Svelte maneja ambos estados. Dentro de {#await data.reviews} se muestra un marcador temporal sencillo como la línea “Cargando reseñas...”, y en la rama {:then reviews} se recorren los resultados. Una rama opcional {:catch error} muestra un mensaje si la solicitud falla.
El usuario ve los detalles del producto casi de inmediato, el área de reseñas muestra un estado de carga breve, y las reseñas reales lo reemplazan en cuanto llegan. No hay código adicional de solicitud del lado del cliente ni gancho de montaje.
Consideraciones sobre la transmisión en flujo
Tenga presentes estas limitaciones:
- La transmisión en flujo funciona a partir de las funciones de carga del servidor. La promesa se crea en el servidor y se transmite desde
+page.server.jso+layout.server.js. Una carga universal de+page.jstambién puede devolver una promesa, pero no se transmite desde el servidor de la misma manera. - Tu adaptador y el servicio de alojamiento deben ser compatibles con respuestas en flujo. Los adaptadores de Node y Vercel lo son, al igual que la mayoría de las plataformas modernas, pero verifica esto en destinos menos comunes. Los proxies que almacenan temporalmente las respuestas también pueden anular silenciosamente esta ventaja.
- Gestiona las rechazos. Una promesa en flujo que se rechaza sin un branch
{:catch}ni un manejador.catch()puede manifestarse como un rechazo no gestionado en el servidor. Proporciona una solución alternativa para las promesas no críticas.
El ciclo de vida completo de la solicitud
Con solicitudes críticas en paralelo, uso deliberado de parent() y transmisión de datos no esenciales, una solicitud a una ruta con muchos datos sigue estos pasos:
- El navegador solicita la ruta.
- SvelteKit ejecuta las funciones de layout y
loadde la página en el servidor. - Todos los datos independientes, como usuarios, publicaciones y comentarios, se obtienen en paralelo.
Compare esto con la versión del lado del cliente desde el principio. En lugar de que el navegador realice tres viajes secuenciales después de que se cargue la página, el servidor los realiza en paralelo antes de enviar cualquier cosa, y generalmente con una latencia mucho menor hacia las fuentes de datos que la que tiene el navegador del usuario.
Lista de verificación previa al lanzamiento para rutas con muchos datos
Revise estas preguntas antes de lanzar una ruta que necesite varios datos:
- ¿Se obtienen los datos para la renderización inicial al montar un componente? Colóquelos en una función
load, generalmente una que se ejecute en el servidor. - ¿Hay varios await independientes uno tras otro dentro de
load? Inicie primero todas las solicitudes y luego espere por ellas conPromise.alloPromise.allSettled. - ¿Es
await parent()la primera línea de la carga de una página? Muévalo al lugar donde realmente se utilicen los datos del padre, y ejecute las solicitudes no relacionadas antes de él. - ¿Hay algún dato que no sea relevante para la primera pintura de la página? Devuélvalo como una promesa sin await y réndalo con
{#await}, incluyendo un bloque de manejo de errores.
fetch de los argumentos load? Solo esa versión resuelve las URLs relativas y reenvía las cookies correctamente durante la renderización en el servidor.Conclusión
Las cascadas de solicitudes no son evidencia de código descuidado. Son lo que ocurre naturalmente cuando la obtención de datos está dispersa por un árbol de componentes. Las funciones load de SvelteKit son menos importantes porque se ejecutan en el servidor y más por el hecho de que agrupan todas las solicitudes necesarias para una página en un único lugar visible, donde se puede decidir cuáles se ejecutan juntas, cuáles dependen realmente de otras y cuáles pueden llegar más tarde. El hábito a desarrollar es sencillo: deja de esperar por reflejo y comienza a esperar intencionadamente. Al aplicarlo a cada ruta, se ahorra tiempo a los usuarios en cada conexión lenta y con cada backend ocupado.
Lecturas relacionadas
- Correlacionar logs en llamadas asincrónicas con AsyncLocalStorage — Aprenda cómo Node.js AsyncLocalStorage sigue el contexto por solicitud, como requestId, más allá de los límites de await, sin tener que pasarlo manualmente por cada función.
- Transferir trabajo intenso de la CPU en Node.js con worker_threads y Pools — Aprenda cómo las worker_threads de Node.js mantienen el bucle de eventos reactivo: creación de workers, comunicación solicitud-respuesta, buffers transferibles, pools y sus problemas comunes.