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.