Streaming internals
Estimated reading time: 2 minutesInitial response
When renderToStream produces the initial HTML, each Suspense boundary renders its
fallback content wrapped in a <div> with a data-sf attribute and a unique ID derived
from the request ID and the boundary's run number.
This div is part of the initial HTML chunk sent to the client. The browser renders it immediately.
Async resolution
When an async child resolves, the runtime pushes two elements to the stream: a
<template> containing the resolved HTML and a <script> that triggers the replacement.
The <template> is inert, so the browser does not render its content. The <script>
executes immediately when the browser receives it, calling the replacement function.
Client-side replacement
The $KITA_RC function is a small script injected before the first Suspense result. It
finds the fallback div by ID, extracts the template's content into a document fragment,
and replaces the div with the fragment. It then removes the template and script elements
from the DOM.
After each replacement, the function scans for any pending templates whose fallback divs were not yet in the DOM when they arrived. This handles cases where a fast async boundary resolves before the initial HTML containing its fallback has been parsed.
HTTP transfer
The response uses chunked transfer encoding. The initial HTML is the first chunk. Each
resolved Suspense boundary produces an additional chunk. The response has no
Content-Length header, so the connection remains open until the last boundary resolves
and the stream is closed.
Framework integrations capture Suspense request state before awaiting an asynchronous root. This keeps replacement chunks available when a fast boundary resolves before a slower root sibling. The resolved root is queued before those buffered replacements, so every replacement follows the fallback it targets.
Express and Fastify integrations also release captured request state when the client disconnects. User promises continue independently, but later settlements cannot write to the closed stream or delete state belonging to another request.
Runtimes like Node.js and Bun handle chunked encoding automatically when data is written
to the response without a Content-Length header.