Stream, don't spin

Flush the static shell at once; stream each slow part into its slot.

Guillermo Rauch@rauchg
tl;DR: everything gets faster, worldwide. Thanks to streaming and leveraging the edge for the initial render, the user rarely would rarely, if ever, see a spinner. (Have you ever seen Google Search results or Amazon render a spinner? No. Because they stream!)
Nov 1, 2023View on X ↗
Guillermo Rauch@rauchg
Why does amazon.com feel so snappy? Zoom out, behold: the power of streaming. … Above the fold, instant. Below? Streamed over time.
Apr 8, 2023View on X ↗
Guillermo Rauch@rauchg
Without PPR: spinner galore With PPR: bliss
Jul 30, 2024View on X ↗
Guillermo Rauch@rauchg
Because of RSC and streaming, we don't need to download MBs of JS and then make a bunch of `fetch` waterfalls to render the dynamic parts. While the skeleton is flushed, and the JS / CSS / fonts are downloaded, the stream from the renderer next to origin database is cooking.
Nov 1, 2023View on X ↗

Demo 1 rebuilds his Amazon point: above the fold instant, the rest streamed in.

Reload the product page

Press Reload and count the spinners on each side.

Client-rendered SPABad
Streamed shellGood
// app/product/[id]/page.tsx: the shell is static, the holes stream in
export default async function Page({ params }) {
  const { id } = await params;
  return (
    <ProductShell id={id}>
      <Suspense fallback={<PriceSkeleton />}><Price id={id} /></Suspense>
      <Suspense fallback={<ReviewsSkeleton />}><Reviews id={id} /></Suspense>
      <Suspense fallback={<RecsSkeleton />}><Recommendations id={id} /></Suspense>
    </ProductShell>
  );
}

async function Price({ id }) {
  const { price, stock } = await getInventory(id); // runs next to the database
  return <PriceTag price={price} stock={stock} />;
}

Scrub both loading sequences

Drag the time slider, or press Play.

Client-rendered SPABad
Streamed shellGood

Why it matters, pros and cons

Why it matters

The shell needs no data, so it leaves the edge first. Slow parts fill their own holes.

Pros

  • First content at TTFB, not after JS and a fetch waterfall.
  • A slow part never holds a fast one; slots resolve alone.
  • Queries start on the server, next to the database.

Cons

  • Streamed slots need fixed sizes, or they shift on arrival.
  • Once the 200 is flushed you can't redirect; fail in-slot.
  • Too many tiny boundaries pop in like confetti; group them.