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
Shell
staticran 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
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
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 fallbackinjected delay 1000ms
Tokyo
suspense fallbackinjected delay 2000ms
Sydney
suspense fallbackinjected delay 4000ms
4 · Runtime request data, also a hole
cookies() call made the entire route dynamic and nothing above could be prerendered. Now it defers only itself.Request context
suspense fallbackstreaming at request time — this is what ships in the static shell