place

Streaming SSR

Pages marked streaming: true render synchronously up to the first <Suspense> boundary, then flush. Slow children inside the boundary continue rendering off the critical path; each one ships a swap chunk as it resolves.

Setup

ts
// Mark the page streaming and wrap the slow part in <Suspense>. page('/feed', { streaming: true, view: () => ( <article> <h1>Feed</h1> <Sidebar /> {/* renders in initial chunk */} <Suspense fallback={<FeedSkeleton />}> {() => <FeedItems />} {/* streams when ready */} </Suspense> </article> ), })

The above renders the sidebar and a feed skeleton in the initial chunk. The feed itself streams in once the data is ready — no extra fetch round-trip, no client JS to coordinate.

How the swap works

ts
// What ships: // // 1. Initial HTML chunk: // // <!--$s:1--> // <div class="feed-skeleton">...</div> // <!--/$s:1--> // // 2. Once FeedItems() resolves, a second chunk: // // <template id="$t:1"><div class="feed-real">...</div></template> // <script> // const t = document.getElementById('$t:1').content // const start = document.querySelector('[data-marker="$s:1"]') // // ... swap into the marker comment range ... // </script> // // 3. Streaming continues until every suspense boundary resolves; the // framework signals "all done" with a final flush chunk.
Why comment markers
The boundaries use HTML comment pairs as the swap range — they survive JS-disabled clients (the fallback simply stays), they don't perturb the layout, and they let the framework identify boundaries without parsing the full DOM tree.

Errors mid-stream

Errors thrown inside a streaming boundary surface to the nearest errorBoundary(); if none, the boundary's fallback stays in place. Synchronous errors before the first flush route to the page's onError.

See also