Next.js Streaming and Suspense

Stream pages with Suspense and loading files so users see content before slow data arrives.

TL;DR

  1. Add loading.tsx to show a fallback while a route loads.
  2. Wrap slow components in <Suspense> to stream them in later.
  3. Enable cacheComponents to prerender a static shell around dynamic parts.

Loading Files

    loading.tsx

    Show instant fallback UI for a whole route segment while its page loads.

    // app/dashboard/loading.tsx
    export default function Loading() {
      return <p>Loading dashboard...</p>;
    }
    Skeleton UI

    Mirror the shape of the real content with simple placeholder blocks.

    export default function Loading() {
      return (
        <div className="animate-pulse space-y-3">
          <div className="h-6 w-1/3 bg-gray-200" />
          <div className="h-40 bg-gray-200" />
        </div>
      );
    }
    Nested Loading

    Add loading files at different levels to scope each fallback to its segment.

    app/
      loading.tsx            // whole app
      dashboard/
        loading.tsx          // dashboard only
        page.tsx

Suspense Boundaries

    <Suspense>

    Wrap one slow component and show a fallback only in that spot.

    import { Suspense } from 'react';
    
    <Suspense fallback={<p>Loading reviews...</p>}>
      <Reviews id={id} />
    </Suspense>
    Separate Boundaries

    Give each slow section its own boundary so they load independently.

    <Suspense fallback={<ChartSkeleton />}>
      <SalesChart />
    </Suspense>
    <Suspense fallback={<TableSkeleton />}>
      <OrdersTable />
    </Suspense>
    Fetch in the Child

    Fetch inside the suspended component so its parent never has to wait.

    async function Reviews({ id }: { id: string }) {
      const reviews = await getReviews(id);
      return <p>{reviews.length} reviews</p>;
    }

Static Shell and Dynamic Parts

    cacheComponents

    Enable Partial Prerendering so a prebuilt static shell is served instantly.

    // next.config.ts
    const nextConfig = {
      cacheComponents: true,
    };
    
    export default nextConfig;
    Runtime Data

    Wrap components that read cookies or headers in Suspense so they stream in.

    import { cookies } from 'next/headers';
    
    async function Greeting() {
      const name = (await cookies()).get('name')?.value;
      return <p>Hello, {name ?? 'guest'}</p>;
    }
    connection()

    Defer a component to request time when it needs a fresh value per visit.

    import { connection } from 'next/server';
    
    async function Now() {
      await connection();
      return <p>{new Date().toLocaleTimeString()}</p>;
    }

Streaming to the Client

    Pass a Promise

    Start a request on the server without awaiting it, then pass the promise down.

    export default function Page() {
      const comments = getComments(); // no await
      return (
        <Suspense fallback={<p>Loading...</p>}>
          <Comments data={comments} />
        </Suspense>
      );
    }
    use()

    Unwrap the streamed promise inside a Client Component with the use hook.

    'use client';
    import { use } from 'react';
    
    export function Comments({
      data,
    }: {
      data: Promise<Comment[]>;
    }) {
      const comments = use(data);
      return <p>{comments.length} comments</p>;
    }
    error.tsx

    Catch a failure in a streamed section without breaking the rest of the page.

    'use client';
    
    export default function Error() {
      return <p>Could not load this section.</p>;
    }

Tips

  1. Place each <Suspense> boundary around the smallest slow section, so the rest of the page appears without waiting.
  2. Match the size of a skeleton to the real content, so the page does not jump when <Suspense> resolves.

Warnings

  1. One <Suspense> boundary around the whole page hides everything until the slowest request finishes, which defeats the point of streaming.
  2. With cacheComponents enabled, reading uncached data outside a <Suspense> boundary triggers a blocking-route error in development.

In Practice

FAQ