Stream behind Suspense

Await slow data inside a Suspense boundary so the shell paints first.

From vercel-react-best-practices, rule async-suspense-boundaries.

One slow query, blank page

Drag the latency; release to reload.

await in PageBad

Waiting for server…

Acme
Revenue
Jul$42.1k
Aug$47.8k
Sep$51.3k
First paint –Data –
await in <Suspense>Good

Waiting for server…

Acme
Revenue
Jul$42.1k
Aug$47.8k
Sep$51.3k
First paint –Data –

export default function Page() {
  return (
    <Shell>
      <Suspense fallback={<RevenueSkeleton />}>
        <Revenue />
      </Suspense>
    </Shell>
  )
}

async function Revenue() {
  const data = await fetchData()
  return <RevenueTable rows={data.rows} />
}

Two readers, one promise

Load both a few times; count requests and reveals.

Each component fetchesBad
<DataDisplay>
Northwind$4,200
Globex$3,150
Initech$2,500
<DataSummary>
$9,850
3 open orders
    Requests –Reveals –
    Shared promise + use()Good
    <DataDisplay>
    Northwind$4,200
    Globex$3,150
    Initech$2,500
    <DataSummary>
    $9,850
    3 open orders
      Requests –Reveals –

      function Page() {
        const dataPromise = fetchData()
      
        return (
          <Suspense fallback={<Skeleton />}>
            <DataDisplay dataPromise={dataPromise} />
            <DataSummary dataPromise={dataPromise} />
          </Suspense>
        )
      }
      
      function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
        const data = use(dataPromise)
        return <div>{data.summary}</div>
      }

      Wrong-size fallbacks shift

      Change rows returned; watch the Pay button move.

      Spinner fallback, 40pxBad
      Invoices
      Pay invoice
      Shift –Score –
      Skeleton of 4 rowsGood
      Invoices
      Pay invoice
      Shift –Score –

      function InvoicesSkeleton() {
        return (
          <ul className="flex flex-col gap-1.5 min-h-[130px]">
            {Array.from({ length: 4 }, (_, i) => (
              <li key={i} className="h-7 rounded bg-muted" />
            ))}
          </ul>
        )
      }
      
      <Suspense fallback={<InvoicesSkeleton />}>
        <Invoices />
      </Suspense>

      Why it matters, pros and cons

      Why it matters

      A top-level await blocks the whole response. Suspense streams the shell first.

      Pros

      • First paint ignores the slowest query
      • The shell is usable while data loads
      • One shared promise, one request

      Cons

      • Data that decides layout, redirects or 404s
      • Fast queries: a flashing skeleton is worse
      • A wrong-height fallback shifts the layout