Docs

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.

TS
// 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.

TS
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.

Proudly built with Nifra — server-rendered on Cloudflare Pages.MIT