Streaming & Suspense Orchestration

Design Suspense boundaries to avoid waterfalls, parallelize slow data, and shape a fast, jank-free progressive render.

Streaming is the payoff of Server Components, but throwing one <Suspense> around everything is amateur hour. The craft is in where the boundaries go and how you kick off data.

Kill the sequential waterfall

The most common performance bug: await A, then await B, then await C in one component. Each waits for the last. If they are independent, start them together with Promise.all so they run in parallel and you wait once for the slowest, not the sum.

Boundary granularity is a UX decision

  • Too coarse (one boundary for the whole page): the user stares at a single big spinner until the slowest thing loads.
  • Too fine (a boundary per tiny element): the page visibly pops and reflows a dozen times — layout jank.
  • Just right: one boundary per meaningful region (feed, sidebar, recommendations), each with a skeleton that matches its final size so nothing shifts.

Preload to overlap

You can call a data function without awaiting it to warm the cache (request memoization), then await it deeper in the tree. This starts the fetch early while other rendering proceeds — a preload pattern that removes hidden waterfalls between a layout and its page.

Example

Example · typescript
// Parallelize independent fetches — one wait, not three.
export default async function Dashboard() {
  // Kick them ALL off before awaiting any
  const usersP = getUsers();
  const salesP = getSales();
  const alertsP = getAlerts();

  const [users, sales, alerts] = await Promise.all([
    usersP,
    salesP,
    alertsP,
  ]);

  return <Overview users={users} sales={sales} alerts={alerts} />;
}

// Or stream each region on its own timeline:
import { Suspense } from 'react';
export function StreamedDashboard() {
  return (
    <>
      <Suspense fallback={<FeedSkeleton />}>
        <Feed />       {/* slow */}
      </Suspense>
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />    {/* fast — paints first */}
      </Suspense>
    </>
  );
}

When to use it

  • A dashboard avoids a data waterfall by using Promise.all inside a Server Component instead of sequential awaits under separate Suspense boundaries.
  • A product page shows a skeleton for slow recommendations while the fast-loading price and gallery appear immediately.
  • A feed page uses multiple independent Suspense boundaries so each post widget streams in the moment its data is ready.

More examples

Parallel fetches avoiding waterfall

Promise.all fires both fetches simultaneously, eliminating the waterfall that sequential awaits would create.

Example · ts
// app/dashboard/page.tsx
import { fetchRevenue, fetchCustomers } from '@/lib/data';

export default async function DashboardPage() {
  // Run in parallel — total time = max(revenue, customers)
  const [revenue, customers] = await Promise.all([
    fetchRevenue(),
    fetchCustomers(),
  ]);
  return (
    <>
      <p>Revenue: {revenue.total}</p>
      <p>Customers: {customers.count}</p>
    </>
  );
}

Independent Suspense for each widget

Each Suspense boundary streams independently so fast widgets appear before the slow recommendations resolve.

Example · ts
import { Suspense } from 'react';
import RevenueCard from './RevenueCard';
import CustomerCard from './CustomerCard';
import SlowRecommendations from './SlowRecommendations';

export default function Dashboard() {
  return (
    <>
      <Suspense fallback={<p>Revenue...</p>}><RevenueCard /></Suspense>
      <Suspense fallback={<p>Customers...</p>}><CustomerCard /></Suspense>
      <Suspense fallback={<p>Loading recs...</p>}><SlowRecommendations /></Suspense>
    </>
  );
}

Avoid nested Suspense waterfall

Sibling Suspense boundaries stream concurrently; nesting them creates a waterfall where B waits for A.

Example · ts
// BAD — nesting causes serial streaming
// <Suspense><A><Suspense><B /></Suspense></A></Suspense>

// GOOD — siblings stream in parallel
export default function Page() {
  return (
    <div>
      <Suspense fallback={<p>A...</p>}><A /></Suspense>
      <Suspense fallback={<p>B...</p>}><B /></Suspense>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.