performance/load-waterfall · Load waterfall
Dependent sequential awaits in a universal load cost a network round trip from the browser per hop.
Severity: warning · Category: performance
What it checks
Flags await chains in a universal load (+page.ts / +layout.ts) where a later await uses the result of an earlier one, whether directly, through destructured bindings, or through intermediate constants. Each dependent hop is a full network round trip from the browser on client-side navigation.
The scan is deliberately conservative:
- It follows the load body’s straight-line statements (directly
try-wrapped ones included) and does not enterifbranches, loops, or nested functions. await parent()is never flagged itself, but data derived from it counts as a dependency.- Reading a response body (
await res.json()and friends, or aPromise.allof such reads) is not a hop, since it costs no extra round trip, though data parsed from it still carries the dependency forward. - A method of an object the load created itself, awaited for its effect alone (
const auth = getAuth(); await auth.initialize()), fills that object: a later await that reads it depends on it. - A container a callback over an earlier result fills (
lists.forEach((l) => ids.add(l.id))) carries that result: a later await that reads it depends on it. - Awaiting a promise that an earlier result hands over, such as one an ancestor load already started and passed down through
parent()(await deferred.state), is not a hop either: no request starts there. A promise this load starts from an earlier result (const p = fetch(user.url), thenawait p) still is. - Dependent chains in server loads are exempt: they cannot be parallelized and already run server-side.
- Loads on a route without client-side rendering are exempt: without a client runtime the universal load only runs during SSR. The route takes the nearest
csrexport along its own+pagefiles and then up its layout chain, as SvelteKit does;csr = falseandcsr = dev(withdevfrom$app/environment,falsein every production build) turn it off, and any othercsrexport turns it back on. Where a node’s universal and server modules both exportcsr, the universal one wins. A layout’s load is exempt only when every page under it hascsroff.
Why it matters
SvelteKit’s performance guidance names request waterfalls as a primary latency source. A universal load re-runs in the browser on client-side navigation, so a chain of N dependent requests costs N sequential round trips on every visit.
Moving the chain to a server load keeps the logic but runs the hops server-to-server, collapsing the client cost to one round trip.
How to fix
Move the dependent chain into a server load:
export async function load({ fetch }) {
const user = await fetch(`/api/user`).then((r) => r.json());
const posts = await fetch(`/api/posts/${user.id}`).then((r) => r.json());
return { user, posts };
}
If part of the data is independent, split it out and parallelize (see performance/sequential-awaits).
Limitations
Only the literal dependent-chain shape is detected; chains hidden behind branches, loops, helper functions, or module-level caches are not. A finding can be silenced per line with // svelte-vitals-disable-next-line performance/load-waterfall.
Mode differences
None. This rule reads source, the same .svelte and .ts files, everywhere it runs. The CLI, the Vite plugin’s build pass, and the live dashboard’s static baseline all report it identically, and the rendered-HTML pass never re-evaluates it. Scoping a run with --route skips it: component-scoped rules have no route to attribute a finding to.
Disabling
export default {
rules: {
'performance/load-waterfall': 'off'
}
};