Rendering & caching harness

Manual test routes for SSG, SSR, ISR, PPR, instant navigation and cache tag invalidation on Next.js 16 with cacheComponents enabled, behind the Pantheon cache handler.

How to read every panel

Each panel shows the scope that produced it, its declared caching policy, and when that scope last executed — a timestamp, a short execution id, and a live age. That is the whole method:

Reload the page and see which stamps moved. A stamp that stays put is a cache hit. A stamp that jumps means the scope re-ran.

The probe panels read an upstream that returns a different integer on every single call, so a cache hit is visible in the content itself and not only in the timestamp. Every API in this harness returns numbers and ISO timestamps only — no free text, no user-submitted content, no imagery.

Read this first

Routes

Self-test — run every check at once

/selftest

Requests every route below twice from the server, compares each panel's execution stamp against what that route guarantees, and reports a verdict with the evidence and a ranked list of what would explain a failure.

check · Press "Test caching". Anything that is not green says what it expected, what it saw, and what would account for the difference.

Route Handler streaming NDJSONdata-exec / data-fallback attributes

upstreams: none — it only reads this app

SSG — fully prerendered

/ssg

Every scope on the page is cached with a long lifetime, so the whole route is static HTML with no dynamic holes.

check · Reload repeatedly. No stamp should ever move. Stamps match the build time until you rebuild.

'use cache'cacheLife('max')

upstreams: frankfurter.dev (settled date) · open-meteo.com

SSR — fully dynamic

/ssr

The mirror image: nothing is cached, every scope defers to request time with connection() inside Suspense.

check · Reload repeatedly. Every stamp and probe value changes every single time.

connection()<Suspense>cookies()headers()

upstreams: random.org · coingecko.com · open-meteo.com

PPR — shell plus holes

/ppr

The default rendering model: a prerendered shell with cached content already in it, and request-time holes that stream in after.

check · Hard-reload with a throttled network. The shell and cached panels paint instantly; the delayed holes fill in at 1s, 2s and 4s.

'use cache'<Suspense>connection()streaming

upstreams: open-meteo.com · coingecko.com

ISR — revalidation windows

/isr

Genuinely-moving upstream data cached at three different speeds, so background regeneration is observable.

check · Watch a panel's age pass its revalidate window, reload to trigger regeneration (you get the stale value), then reload again to see the new stamp.

cacheLife('isr-15' | 'isr-60' | 'blog' | 'hours')stale-while-revalidate

upstreams: coingecko.com · open-meteo.com · frankfurter.dev · api.github.com

Cache tags and invalidation

/tags

The same tagged data invalidated four different ways, plus a real mutation to prove read-your-own-writes.

check · Precision vs speed: revalidateTag(tag, 'max') re-executes only the tagged scope but needs two reloads; the immediate calls (updateTag, {expire: 0}) move every cached scope on the route. refresh() moves nothing.

cacheTag()updateTag()revalidateTag()revalidatePath()refresh()

upstreams: open-meteo.com · in-app mutable store

cacheLife profiles

/cache-life

Every built-in and custom profile side by side, including four probes that straddle the documented prerender thresholds.

check · After a build, check which probes shipped in the static HTML: stale < 30s and expire < 5min become dynamic holes instead.

cacheLife()prerender thresholdsApp Shell eligibility

upstreams: random.org

Cache scopes

/scopes

'use cache' vs 'use cache: remote' vs 'use cache: private' vs the legacy tagged fetch — the same numeric probe through four different storage paths.

check · Open in two different browsers. The shared and remote panels show the same number in both; the private one differs per client.

'use cache''use cache: remote''use cache: private'fetch force-cache

upstreams: random.org

Streaming and waterfalls

/streaming

Sequential awaits against parallel ones, with staggered upstream delays, and Suspense boundaries at three granularities.

check · The sequential column takes the sum of its delays; the parallel column takes the max. Per-item boundaries fill one at a time.

<Suspense>Promise.allstreaming order

upstreams: open-meteo.com + injected delay

ISR fallback and App Shell upgrade

/isr-fallback

generateStaticParams prerenders two regions and one station each; everything else gets the App Shell instantly, then upgrades in the background.

check · Count Suspense fallbacks per URL shape: none when both params are known, one for a known region with an unlisted station (a partial shell), two when neither is known — and none on the second visit.

generateStaticParams()partialPrefetchingApp Shellparams in Suspense

upstreams: open-meteo.com

Instant navigation

/instant

The instant route segment config, with tabs that validate clean and one that deliberately blocks.

check · In dev, the blocking tab raises an insight in the error overlay naming the component. In next start, tab switches paint with no gap.

export const instantNavigation Inspector<Suspense>

upstreams: open-meteo.com + injected delay

Proxy (was middleware)

/proxy

proxy.ts branches for direct responses, redirects, rewrites, header injection and A/B bucketing — plus what it deliberately cannot do.

check · Proxy runs even for statically prerendered routes: curl -sI /ssg three times and x-proxy-invocation differs every time while the body never changes.

proxy.tsmatcher has/missingNextResponsewaitUntilNode.js runtime

upstreams: none — proxy must not fetch data

Prefetch behaviour

/prefetch

Segment-level prefetch config and Link prefetch props, with a network-tab recipe for seeing what each one actually requests.

check · In next start with the network tab open, scroll the links into view and compare requests for partial, default and force-disabled destinations.

export const prefetch<Link prefetch>partialPrefetching

upstreams: open-meteo.com

Intercepting routes

/intercept

One URL rendered as a modal on client navigation and as a full page on a hard load, selected by the Next-Url request header.

check · Click a photo for the modal, refresh for the full page. With curl, RSC requests with and without Next-Url: /intercept redirect to different _rsc values and return different payloads.

@slot parallel routes(.) intercepting routesdefault.tsxNext-Url

upstreams: none

Route handlers

These are the in-app APIs. They give you a request-time signal that no external service can rate limit, and a webhook-shaped way to invalidate tags from outside the app.

Self-test, as a stream

open

The self-test suite as newline-delimited JSON, one event per check. GET is read-only; POST with ?confirm=mutate-cache also runs the invalidation scenarios.

curl -N 'localhost:3000/api/selftest?suite=quick'

Per-request timestamp

open

Uncached route handler. Supports ?delay=<ms> to simulate a slow backend.

curl -s localhost:3000/api/now

Mutable store

open

GET reads the in-app store; POST adjusts stock and invalidates the inventory tag.

curl -s localhost:3000/api/inventory

Webhook-shaped invalidation

open

revalidateTag from a route handler, where updateTag is unavailable. Accepts ?tag=, ?path= and ?mode=stale|now.

curl -s 'localhost:3000/api/revalidate?tag=stations&mode=now'

Guarded by proxy.ts

open

The proxy returns 401 directly and this handler never runs. Add the authorization header to let it through.

curl -si localhost:3000/api/proxy-guard | head -5

Cached route handler

open

A route handler wrapping its work in 'use cache', tagged so it can be invalidated.

curl -s localhost:3000/api/cached-time

Further reading

TESTING.md in the repo root has the full route table, the shell one-liners for reading stamps out of the markup, the cacheLife prerender thresholds, and the behaviours that were measured in this app — including the trade-off between revalidateTag(tag, 'max') and the immediate invalidation calls, which is not obvious from the API surface.