CPCODELAB
Next.js

Streaming and Suspense in the App Router

11 min read

Stream HTML as It Becomes Ready

By default, Next.js waits for all data in a page to be fetched before sending any HTML to the browser. Streaming lets you break the page into chunks and send each chunk as it is ready, so users see content sooner — even for slow data sources.

Wrap any slow Server Component in <Suspense> with a fallback prop. Next.js will immediately stream the shell of the page and the fallback, then stream in the real content when it is ready.

tsx
// src/app/dashboard/page.tsx
import { Suspense } from "react";
import StatsSkeleton from "@/components/StatsSkeleton";
import RecentOrders from "@/components/RecentOrders"; // slow — hits the DB

export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
      {/* Fast content renders immediately */}
      <p>Welcome back!</p>

      {/* Slow component is streamed in later */}
      <Suspense fallback={<StatsSkeleton />}>
        <RecentOrders />
      </Suspense>
    </main>
  );
}

The loading.tsx Shortcut

A loading.tsx file in a route segment is syntactic sugar for wrapping the entire page.tsx in <Suspense>. Use loading.tsx for full-page loading states; use inline <Suspense> boundaries for more granular streaming.

tsx
// src/components/RecentOrders.tsx — async Server Component
import { db } from "@/lib/db";

export default async function RecentOrders() {
  // This fetch delays rendering — Next.js streams it in when done
  const orders = await db.order.findMany({ take: 5, orderBy: { createdAt: "desc" } });
  return (
    <ul>
      {orders.map((o) => (
        <li key={o.id}>{o.id} — {o.status}</li>
      ))}
    </ul>
  );
}

Place <Suspense> boundaries as close to the slow component as possible. Each boundary is an independent stream chunk — the more you nest, the finer-grained the streaming.

Ready to test yourself?
Practice Next.js— quiz & coding exercises