Suspense and Streaming

Streaming Parts of a Page

loading.tsx covers the whole page. With <Suspense> you choose smaller pieces. The server sends the HTML it has, then streams each suspended part when its data is ready, over the same response.

import { Suspense } from 'react';
import Reviews from './Reviews';
import ReviewsSkeleton from './ReviewsSkeleton';

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <>
      <ProductInfo id={id} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews id={id} />
      </Suspense>
    </>
  );
}

Streaming Simulated

Example

The server writes the shell first, then each part in the order its data finishes (not the order in the page):

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function section(name: string, ms: number): Promise<string> {
  await sleep(ms);
  return '<section>' + name + '</section>';
}

async function streamPage(write: (chunk: string) => void) {
  write('<html><body><h1>Redmi Note 14</h1>');
  write('<div id="s1">Loading reviews...</div><div id="s2">Loading similar...</div>');
  const parts: [string, Promise<string>][] = [
    ['s1', section('Reviews (4.3★, 12,480 ratings)', 120)],
    ['s2', section('Similar products', 40)],
  ];
  await Promise.all(parts.map(async ([id, p]) => write('<template for="' + id + '">' + (await p) + '</template>')));
  write('</body></html>');
}

let n = 0;
streamPage((chunk) => console.log('chunk ' + ++n + ': ' + chunk));

Output

chunk 1: <html><body><h1>Redmi Note 14</h1>
chunk 2: <div id="s1">Loading reviews...</div><div id="s2">Loading similar...</div>
chunk 3: <template for="s2"><section>Similar products</section></template>
chunk 4: <template for="s1"><section>Reviews (4.3★, 12,480 ratings)</section></template>
chunk 5: </body></html>

Benefits

  • Faster first paint (better TTFB and FCP): the slowest query no longer blocks the page.
  • Works without client JavaScript for the content itself; the browser just receives more HTML.
  • SEO safe: crawlers receive the full streamed HTML.