PPR — static shell plus streaming holes

The top two rows are prerendered and arrive with the first byte. The bottom row is three request-time holes with injected delays of one, two and four seconds.

Holes that streamed in are highlighted briefly as they settle, so you can see which parts arrived late even after the page has finished loading.

What to check

In a build (pnpm build && pnpm start), hard-reload with the network tab open. The shell and both cached panels paint immediately; the three station panels replace their fallbacks at roughly 1s, 2s and 4s.

To see the shell in isolation, watch the raw stream: curl -N --raw -s localhost:3000/ppr. The first flush contains the headings and the fallbacks; the station markup arrives in later chunks. Or list just the fallbacks: curl -s localhost:3000/ppr | grep -o 'data-fallback="[^"]*"' .

In dev, open Next.js DevTools → Navigation Inspector and turn on Pause on navigations to freeze the page at its instant UI.

1 · In the static shell

No data access at all, so this is plain prerendered HTML.

Shell

static

ran at exec gy1s0gage …

Everything on this page that is not inside a Suspense boundary or a short-lived cache scope arrives with this markup.

Ships in the first flush of the response, before anything is fetched.

2 · Cached, also in the prerendered HTML

Both are use cache scopes with lifetimes long enough to be prerendered, so their content is in the initial HTML — no fallback, no streaming, but still revalidated in the background on their own schedule. Note that being in the prerendered HTML is not the same as being in the reusable App Shell; see /cache-life.

Stations — cached

use cache
cacheLife
blog
cacheTag
stations, stations:index
upstream
477ms

ran at exec sxq6mqage …

  • London17.7°C · 71% · 1020.3hPa
  • Paris19.1°C · 64% · 1018.5hPa
  • Reykjavik10.1°C · 84% · 963.2hPa
  • Tokyo20.2°C · 96% · 1012.5hPa
  • Sydney16.3°C · 91% · 1018.6hPa

cacheLife('blog'): prerendered, though its 60s stale keeps it out of the reusable App Shell. See /cache-life.

Market — cached, 15s window

use cache
cacheLife
isr-15
cacheTag
market
upstream
134ms

ran at exec 825j23age …

  • bitcoin$84,324 · published 20:59:10Z
  • ethereum$2,686.89 · published 20:59:20Z

Prices are genuinely moving upstream, but this panel pins them for 15 seconds. Watch its age cross 15s, then reload twice.

3 · Dynamic holes, streamed after

Each panel awaits connection(), then an injected delay, then a real upstream call. Their <Suspense> fallbacks are what the shell contains; the real content is appended to the same response as it resolves.

Paris

suspense fallback

injected delay 1000ms

Tokyo

suspense fallback

injected delay 2000ms

Sydney

suspense fallback

injected delay 4000ms

4 · Runtime request data, also a hole

This is the change that makes PPR worth having. Before Cache Components, one cookies() call made the entire route dynamic and nothing above could be prerendered. Now it defers only itself.

Request context

suspense fallback

streaming at request time — this is what ships in the static shell