Streaming
Nifra streams HTML as it renders — the shell goes out first, slow data fills in. It's a Web ReadableStream, so it works on Bun, Node, Deno, and the edge (workerd).
Suspense & defer()
Wrap slow data in defer() in the loader and render it through <Await> (a Suspense boundary). The client receives the shell + a streamed resolution, then hydrates — no waterfall, no blank screen.
// Send the page shell immediately; stream the slow part in when it resolves.
export async function loader({ api }: LoaderArgs<typeof app>) {
return {
user: (await api.users({ id: "7" }).get()).data, // awaited — in the shell
feed: defer(api.feed.get()), // deferred — streamed later
}
}
export default function Page(props: { data: LoaderData<typeof loader> }) {
return (
<>
<h1>{props.data.user?.id}</h1>
<Await resolve={props.data.feed} fallback={<p>Loading feed…</p>}>
{(feed) => <Feed items={feed} />}
</Await>
</>
)
}The same defer() works in actions and across client-side soft navigations (an NDJSON stream settles the deferred values), and it's framework-agnostic — React <Suspense> and Solid's streaming both drive it from one core.
Server-Sent Events
For server push — live feeds, progress, notifications — sse(c, run) returns a text/event-stream response a handler returns directly. Push frames with stream.send({}); the connection stays open until run resolves, you call stream.close(), or the client disconnects (stream.signal). It's a Web ReadableStream too, so it runs on Bun, Node, Deno, and the edge — no new Function, no per-runtime API.
import { sse } from "@nifrajs/core"
// A live feed — push events until the client disconnects.
app.get("/notifications", (c) =>
sse(c, (stream) => {
const off = notifications.subscribe((n) =>
stream.send({ event: "notification", id: n.id, data: JSON.stringify(n) }),
)
// Keep the connection open until the client leaves, then tear down.
return new Promise<void>((resolve) =>
stream.signal.addEventListener("abort", () => { off(); resolve() }, { once: true }),
)
}, { keepAlive: 15_000 }),
)event:, id:, and retry: are supported (and CR/LF is stripped from event/id to prevent frame injection); multi-line data is split into multiple data: lines per the spec; and keepAlive emits comment pings so idle proxies don't drop the connection.