Composition (route × Suspense × ISR × fetch-cache)
Every cache layer stacking on one page. The outer timestamp
ticks per request (force-dynamic). Three Suspense boundaries
stream their real content as it settles; each wraps an
<ISR> fragment with a different TTL. The
slowest fragment additionally fetches with
next.revalidate, so layered caching keeps even
the outer-miss render cheap.
Output (refresh repeatedly to watch the cadences)
Outer (live): 2026-05-05T09:18:19.830Z
- loading fast…
- loading medium…
- loading slow…
Cadence map
| Layer | Cadence | Cache |
|---|---|---|
| Outer | every request | — |
| Fast fragment | ~5s | FragmentCache (timed) |
| Medium fragment | ~15s | FragmentCache (timed) |
| Slow fragment | ~30s | FragmentCache (timed) |
| Slow's upstream fetch | ~8s (only on slow miss) | FetchCache + tag "composition" |
Source
import { Suspense } from "@bext-stack/framework/streaming";
import { ISR } from "@bext-stack/framework/cache";
export const dynamic = "force-dynamic";
export default function Page() {
const outer = new Date().toISOString();
return (
<div>
<p>Outer (live): {outer}</p>
<Suspense fallback={<p>loading fast…</p>}>
<ISR cacheKey="composition:fast" ttl={5}>
{async () => {
await new Promise(r => setTimeout(r, 200));
return `<p>Fast (5s ISR): ${new Date().toISOString()}</p>`;
}}
</ISR>
</Suspense>
<Suspense fallback={<p>loading medium…</p>}>
<ISR cacheKey="composition:medium" ttl={15}>
{async () => {
await new Promise(r => setTimeout(r, 400));
return `<p>Medium (15s ISR): ${new Date().toISOString()}</p>`;
}}
</ISR>
</Suspense>
<Suspense fallback={<p>loading slow…</p>}>
<ISR cacheKey="composition:slow" ttl={30}>
{async () => {
await new Promise(r => setTimeout(r, 800));
// fetch with its own 8s next-cache layer.
const r = await fetch("http://127.0.0.1:3032/api/echo?key=composition", {
next: { revalidate: 8, tags: ["composition"] },
});
const data = await r.json();
return `<p>Slow (30s ISR + 8s fetch-cache): ${data.receivedAt}</p>`;
}}
</ISR>
</Suspense>
</div>
);
}