Back to Blog
Next.jsJune 28, 20264 min read

Next.js App Router Parallel Data Fetching with Suspense

Optimize your Next.js App Router performance by implementing parallel data fetching. Learn how to use React Suspense to eliminate request waterfalls.

Next.jsApp RouterReactPerformanceWeb DevelopmentServer Components

When I first moved a legacy dashboard to the Next.js App Router, I hit the classic "waterfall" trap. I was awaiting three separate API calls in a row, leading to a total response time that was the sum of all three requests. It felt sluggish, and honestly, it was an embarrassing performance hit.

Fixing this requires shifting how you think about data orchestration in the App Router. By decoupling your fetching logic, you can fire requests simultaneously and use React Suspense to handle the streaming UI.

Identifying the Waterfall

In a standard Server Component, it’s tempting to write code that looks like this:

TSX
async function Dashboard() {
  const user = await getUser(); // Takes 200ms
  const posts = await getPosts(); // Takes 300ms
  
  return <Layout user={user} posts={posts} />;
}

Because await blocks the execution, your user is waiting 500ms before anything hits the browser. If your getPosts call doesn't actually depend on the user object, this is wasted time. We’ve all been there, and it’s usually the first thing that needs refactoring when building Next.js Server Components: Architecting Resilient Data Fetching Pipelines.

Implementing Parallel Data Fetching

To achieve true parallel data fetching, you need to initiate the promises before awaiting them. In modern JavaScript, Promise.all is your best friend.

TSX
async function Dashboard() {
  const userPromise = getUser();
  const postsPromise = getPosts();

  const [user, posts] = await Promise.all([userPromise, postsPromise]);
  
  return <Layout user={user} posts={posts} />;
}

Now, the total response time is only as long as the slowest request—around 300ms instead of 500ms. This is a massive win for perceived performance. However, you still have to wait for the slowest item before the page renders at all.

Streaming with React Suspense

To make the UI feel even snappier, you should break these components into smaller, independent units. By wrapping these in Suspense boundaries, you allow Next.js to stream the shell of your page immediately while the data fetches in the background.

TSX
export default function Page() {
  return (
    <main>
      <Suspense fallback={<UserSkeleton />}>
        <UserComponent />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsComponent />
      </Suspense>
    </main>
  );
}

This pattern is essential when you're looking at Next.js App Router Data Fetching: Avoiding Performance Waterfalls. By isolating the fetching logic inside UserComponent and PostsComponent, the requests fire in parallel the moment the page starts rendering.

Performance Trade-offs and Considerations

While I love this architecture, it isn't a silver bullet. Here’s a quick comparison of how you might handle data:

StrategyComplexityPerformanceUX
Sequential AwaitLowPoorBlocking
Promise.allMediumGoodAll-or-nothing
Suspense StreamingHighExcellentIncremental

If you have a complex multi-tenant application, you might also need to consider Next.js Multi-tenancy: Implementing Tenant-Aware Data Sharding to ensure that your parallel requests stay performant across different database shards.

One caveat: don't over-fragment your components. If you have 20 Suspense boundaries on a single page, you might end up with "layout shift hell" as content pops in at different times. I usually group related data into a single fetch if the UI components need to render together anyway.

Common Pitfalls

  1. Over-fetching: Just because you can fetch in parallel doesn't mean you should fetch everything at once. Keep an eye on your database connection pool limits.
  2. Missing Loading States: If you don't provide a good fallback prop in your Suspense boundary, the user might see a blank screen. Spend the time on good skeletons.
  3. Cache Invalidation: Remember that concurrent requests might hit your cache differently. If you're using fetch with next: { tags: [...] }, ensure your revalidation strategy is robust, as discussed in Next.js App Router Data Revalidation: Mastering Cache Tags at Scale.

FAQ

Does parallel fetching increase server load? Yes, it can. You're executing multiple requests at once rather than queuing them. Ensure your database or external API can handle the concurrent connections.

Can I use Promise.all inside a Client Component? No. Promise.all is a server-side optimization pattern for Server Components. Client-side fetching should generally use useQuery or useSWR for better state management.

Is there a limit to how many Suspense boundaries I should use? Technically no, but keep it sane. Too many boundaries can lead to fragmented UI updates that feel chaotic to the user.

Next time you're profiling your page load, look for those long, sequential bars in your network tab. I'm still experimenting with how much granularity is "too much" for mobile users on high-latency connections, but for most dashboards, moving to parallel fetches with Suspense is the single biggest performance improvement you can make.

Similar Posts