Streaming SSR

Wrap a page in Streamed.of(() -> ...) and the shell flushes to the browser immediately. Every Suspense block renders its loading placeholder instantly and streams its real HTML into the page the moment its data resolves — blocks load in parallel and arrive in completion order. No JavaScript is written.

Usage

app.get("/dashboard", req -> Streamed.of(() ->
    new Layout("Dashboard", div(
        header(),                              // paints immediately
        Suspense.of(() -> reports.slowQuery()) // streams in when ready
            .loading(() -> spinner("Crunching numbers..."))
            .render(data -> reportTable(data))
    )).render()));

How It Works

  • The shell (with placeholders) is written and flushed first — TTFB ~35ms
  • Each Suspense block runs on a virtual thread, in parallel
  • Completed blocks stream as <template> chunks plus a tiny swap script
  • Total time = the slowest block, not the sum
The page must be built inside the supplier — the element DSL evaluates eagerly, so a pre-built tree would already have executed its Suspense blocks before streaming starts.
See it live: /demo/streaming renders a 400ms and a 1200ms block. Outside a Streamed page, Suspense behaves exactly as before (blocking or nonBlocking).

When To Use It

Streaming shines when a page mixes fast content (navigation, headers, cached data) with slow content (reports, external APIs, heavy queries). The user sees and can interact with the page immediately instead of staring at a blank tab for the slowest query's duration.