I’ll fetch it all first, then render.
Not any more. The static shell ships immediately, and each slow part leaves behind a fallback — the gray placeholder you’ll watch below. As each part finishes, the server streams its finished HTML down the same response, and the placeholder gives way. The fast parts don’t wait for the slow ones.
These three rows are slow on purpose. The delays are hardcoded — the only faked thing on this page — but the streaming is not: each row is a Server Component that genuinely finishes on the server and lands when it is done. Run it again and watch the order hold. What you are seeing is the server finishing, not an animation pretending to.
cooking (~400 ms)
cooking (~1100 ms)
cooking (~1900 ms)
run #0 · via ?stream= in the URL
slow-row.tsx
// slow-row.tsx — genuinely slow, on the server, per request
export async function SlowRow({ delayMs, label }) {
await connection(); // request-time work starts here
await sleep(delayMs);
return <Row label={label} landedAt={new Date()} />;
}
// in the page — the shell ships instantly, rows land when done.
// A new run id makes new boundaries, so the fallbacks show again.
<Suspense fallback={<RowSkeleton />} key={`run-${run}-${row.label}`}>
<SlowRow delayMs={row.delayMs} label={row.label} />
</Suspense>