exequiel-sosa
alal
← Back to blog
[nextjs]July 20, 2026· 4 min read

Eliminate Data Waterfalls in Next.js with Parallel Fetching and Suspense

Learn how to break data waterfalls in Next.js App Router using parallel fetches, Server Components, and React Suspense for faster, smoother user experiences.

#performance#react#nextjs#suspense#fetch

Why data waterfalls still exist in modern Next.js apps

Even with the App Router and Server Components, many teams fall back to the old pattern of fetching data in a parent component, then passing it down to children. When Component B waits for Component A's await, the browser stalls on two sequential network round‑trips. The result is a visible loading spinner that could have been avoided with a few minutes of refactoring.

"A waterfall is a performance killer because each await adds the full latency of the request. Parallelism cuts that latency in half (or more)."

Parallel fetches with the App Router

Next.js 13+ lets you run fetch calls at the top level of a Server Component. By default, those calls are concurrent when you use Promise.all. The trick is to keep the data fetching logic close to the component that actually needs it, rather than bubbling everything through a single layout.js.

// app/dashboard/page.tsx
export default async function DashboardPage() {
  const [stats, recent] = await Promise.all([
    fetch('https://api.example.com/stats').then(r => r.json()),
    fetch('https://api.example.com/recent').then(r => r.json()),
  ]);

  return (
    <section>
      <Stats data={stats} />
      <RecentActivity data={recent} />
    </section>
  );
}

This pattern eliminates the waterfall entirely: both requests start at the same time, and the page renders once the slower of the two resolves.

Decoupling UI with React Suspense

When you need to render a part of the UI before all data is ready, wrap the slow component in Suspense. The fallback can be a skeleton or a simple spinner, but the rest of the page is already interactive.

// app/dashboard/RecentActivity.tsx
import { Suspense } from 'react';

export default function RecentActivity() {
  return (
    <Suspense fallback={<div>Loading recent activity…</div>}>
      <RecentList />
    </Suspense>
  );
}

async function RecentList() {
  const data = await fetch('https://api.example.com/recent').then(r => r.json());
  return (
    <ul>
      {data.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

Because RecentList is a Server Component, the fetch runs on the server, and the client only receives the rendered HTML once it’s ready. Meanwhile, the surrounding UI stays responsive.

When to keep fetching on the client

Not every request belongs on the server. If the data is user‑specific, changes frequently, or depends on client‑only state (like window size), fetch it in a Client Component. Even then, avoid chaining:

  • Use useEffect with Promise.all for parallel calls.
  • Leverage SWR or React Query's useQueries to orchestrate multiple queries.

Example with SWR:

import useSWR from 'swr';
const fetcher = url => fetch(url).then(r => r.json());

export default function DashboardClient() {
  const { data: stats } = useSWR('/api/stats', fetcher);
  const { data: recent } = useSWR('/api/recent', fetcher);
  // Both requests fire in parallel automatically.
  return (stats && recent) ? (
    <>
      <Stats data={stats} />
      <RecentActivity data={recent} />
    </>
  ) : <div>Loading…</div>;
}

Practical trade‑offs you’ll hit at scale

Parallel fetching sounds free, but there are limits:

  1. API rate limits – firing dozens of requests simultaneously can hit throttling. Batch where possible.
  2. Cache fragmentation – each fetch creates its own cache entry. Use consistent revalidate settings to avoid stale data.
  3. Server load – Server Components run on the edge or Vercel serverless functions. Parallelism increases CPU burst, which can affect cold start times.

Balancing these concerns means you sometimes group related data into a single endpoint, or employ GraphQL to let the server resolve the joins.

Takeaway checklist

  • Identify sequential await chains in Server Components.
  • Replace them with Promise.all or parallel SWR queries.
  • Wrap slow parts in Suspense to keep the UI interactive.
  • Monitor API rate limits and consider batching for high‑traffic routes.

Apply these patterns, and you’ll shave off 100‑200 ms per page load—enough to keep users from staring at spinners and more likely to boost Core Web Vitals.

// related

find me in:
linkedin
X
facebook