Request waterfalls: how sequential awaits slow your page down
Why sequential awaits in Server Components slow a page down, and how to fix it with Promise.all and Suspense.
The page is slow, yet every request is fast on its own. Often the cause is not the requests themselves but their order: each one waits for the one before it. This is called a request waterfall.
What the problem looks like
export default async function Dashboard() {
const user = await getUser(); // 300 ms
const orders = await getOrders(); // 400 ms
const stats = await getStats(); // 500 ms
return <DashboardView user={user} orders={orders} stats={stats} />;
}The code is clean and easy to read. But every await pauses the next line, so the
page waits 1200 ms. These three requests do not depend on each other, so they could
have started together.
First fix: Promise.all
export default async function Dashboard() {
const [user, orders, stats] = await Promise.all([
getUser(),
getOrders(),
getStats(),
]);
return <DashboardView user={user} orders={orders} stats={stats} />;
}Now all three requests go out at once and the page waits only for the slowest one, 500 ms.
Two things to keep in mind. If one request fails, Promise.all rejects as a whole;
use Promise.allSettled when partial results are acceptable. And the user still sees
nothing until the slowest request finishes.
Second fix: split with Suspense
When each section fetches its own data, the sections run in parallel and each one appears as soon as it is ready:
import { Suspense } from "react";
export default function Dashboard() {
return (
<>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<Orders />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
</>
);
}
async function Orders() {
const orders = await getOrders();
return <OrderList orders={orders} />;
}The header shows at 300 ms, the orders at 400 ms, the stats at 500 ms. The total time is the same, but the user is no longer staring at an empty screen.
When requests really do depend on each other
Sometimes the order is required: you need the user ID before you can fetch the orders. In that case, start the independent requests early:
const statsPromise = getStats(); // started, not awaited yet
const user = await getUser();
const orders = await getOrders(user.id);
const stats = await statsPromise;If several components need the same data, wrap the function in React's cache.
The request then runs only once per render:
import { cache } from "react";
export const getUser = cache(async () => {
return db.user.current();
});How to spot one
- Look for several
awaits in a row in one function and check whether each one actually needs the previous result. - A parent that awaits and then renders a child that also awaits is a waterfall too: the child cannot start until the parent finishes.
- Log how long each request takes. If the sum equals the page time, they are running one after another.
Takeaway
Making a request faster is hard; changing the order is easy. Run independent requests in parallel first, then move the slow sections behind Suspense. In many cases that alone makes the page feel several times faster.