Skip to content
C.W.K.
Stream
Lesson 07 of 09 · published

Streaming & Suspense

~22 min · streaming, Suspense, loading

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

Why streaming exists

Without streaming, the server has to wait for every piece of data before sending the first byte of HTML. With streaming, the framework sends the shell + cheap parts immediately, and streams the slower parts as their data resolves.

The two ways to stream

ApproachGranularity
loading.tsxWhole route segment — framework wraps it in Suspense for you
<Suspense fallback>Per component — you choose what streams independently

How it feels to the user

The page header and navigation appear instantly; the slow chart shows its skeleton; when the chart's data arrives, the skeleton swaps for the real content — without re-rendering the page above.

Place Suspense boundaries between units that have different latency and can remain understandable on their own. Make fallbacks roughly match final dimensions, keep the primary heading and actions outside slow regions, and stream in the order users can productively consume. When splitting slow boundaries, decide how much meaningful content appears first. Streaming loses its benefit if numerous fallbacks still leave the user unsure what is loading. Streaming changes perceived waiting, not the duration of a slow query. Skeletons around an unchanged waterfall can make the interface busier while server cost remains identical. Optimize the cause and the presentation as separate tasks.

Inject distinct delays into the shell, list, and chart. Record time to first byte, time to each region, and full completion under route-level and component-level boundaries. Check layout shift and announce loading state to assistive technology.

Code

Whole-segment streaming·tsx
// app/dashboard/loading.tsx — shown instantly
export default function Loading() {
  return <DashboardSkeleton />;
}

// app/dashboard/page.tsx — streams when its data arrives
export default async function Dashboard() {
  const data = await fetchSlow();
  return <DashboardContent data={data} />;
}
Granular Suspense islands·tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div className="space-y-6">
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

External links

Exercise

Take a slow data-heavy page and split it into three Suspense boundaries. Capture before/after timings: how fast does the user see the first paint?

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.