Parallel & Sequential Fetching
When a Server Component needs data from more than one source, how you write the awaits determines whether those requests happen one after another or all at once. Getting this wrong is one of the most common performance mistakes in App Router apps — it is easy to write requests sequentially by accident and end up with a page that is far slower than it needs to be.
Sequential fetching (the request waterfall)
If one piece of data depends on the result of another, sequential fetching is required — you have no choice. But if the two requests are independent, awaiting them one at a time still forces the second request to wait for the first to finish, even though nothing about it actually depends on that result.
async function getUser(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}`)
return res.json()
}
async function getRecommendations() {
const res = await fetch('https://api.example.com/recommendations')
return res.json()
}
export default async function ProfilePage({ params }: { params: { userId: string } }) {
// getRecommendations() does not depend on the user at all,
// but this still waits for getUser() to fully finish first.
const user = await getUser(params.userId)
const recommendations = await getRecommendations()
return (
<>
<h1>{user.name}</h1>
<RecommendationList items={recommendations} />
</>
)
}Parallel fetching with Promise.all
When requests are independent, kick them both off first, then await them together with Promise.all. The underlying network requests happen concurrently, and the total wait time is roughly the duration of the slowest request, not the sum of both.
export default async function ProfilePage({ params }: { params: { userId: string } }) {
// Both requests start immediately, without waiting on each other
const [user, recommendations] = await Promise.all([
getUser(params.userId),
getRecommendations(),
])
return (
<>
<h1>{user.name}</h1>
<RecommendationList items={recommendations} />
</>
)
}With the same 300ms and 400ms requests, the parallel version finishes in roughly 400ms — the slower of the two — instead of 700ms.
Genuinely sequential (dependent) data
Some data really does depend on a previous result, and there is no way around awaiting it in order. That is fine — the goal is not to eliminate all sequential awaits, only the accidental ones.
export default async function OrderPage({ params }: { params: { orderId: string } }) {
// shippingAddress genuinely depends on the order's customerId,
// so this sequence is unavoidable and correct.
const order = await getOrder(params.orderId)
const shippingAddress = await getAddress(order.customerId)
return <OrderSummary order={order} address={shippingAddress} />
}Parallel vs. sequential at a glance
Pattern | Total time (roughly) | Use when |
|---|---|---|
Sequential awaits | Sum of every request | Request B genuinely needs a value from request A |
Promise.all([...]) | Duration of the slowest request | Requests are independent of each other |
Streaming in parallel with Suspense
For an even better perceived experience, independent data-fetching sections of a page can each be wrapped in their own Suspense boundary and rendered by a separate async component. Next.js streams each section in as soon as its own data is ready, instead of waiting for the slowest one to block the entire page.
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<>
<Suspense fallback={<p>Loading profile…</p>}>
<Profile />
</Suspense>
<Suspense fallback={<p>Loading recommendations…</p>}>
<Recommendations />
</Suspense>
</>
)
}