Skip to content
Vincle

Loading…

    Streaming

    Send the HTML shell immediately and stream deferred fragments inline in the same HTTP response. One connection, progressive delivery: no client-side fetch logic required.

    When to stream#

    Streaming is the right choice when:

    • Time-to-first-byte matters (the shell paints before slow queries finish)
    • Fragments depend on request-time data (user-specific content, live results)
    • You want a single HTTP connection from shell through last fragment
    • The client has no JS runtime (or you want to minimise it)

    If your content is known at build time and you want zero server infrastructure, use Static generation instead.

    renderToStream#

    Returns a ReadableStream<string>. The adapter is required: pass a streaming adapter (e.g. NativeAdapter) as the second argument. NativeAdapter injects its small polyfill only when fragments are present.

    import { Slot, Defer, renderToStream } from "@vincle/flow";
    import { NativeAdapter } from "@vincle/flow/adapters";
    declare function fetchComments(): Promise<{ text: string }[]>;
    async function Comments() {
    const items = await fetchComments();
    return (
    <ul>
    {items.map((c) => (
    <li>{c.text}</li>
    ))}
    </ul>
    );
    }
    // <Slot> declares the placeholder with fallback content in the shell.
    // <Defer> pushes the real content, which replaces it when resolved.
    function Page() {
    return (
    <html>
    <body>
    <h1>My page</h1>
    <Slot name="comments">
    <p>Loading comments…</p>
    </Slot>
    <Defer target="comments">{() => <Comments />}</Defer>
    </body>
    </html>
    );
    }
    const stream = renderToStream(() => <Page />, NativeAdapter);

    The shell is rendered fully, then sent as a single chunk. Each fragment follows as soon as it resolves. renderToStream returns the stream only: headers are yours to set, or use serve(), which sets them.

    To send the stream over HTTP, with serve() or a Node.js server (Express, Fastify, Hono, anything built on node:http), see HTTP serving.

    Async iterables: streaming sequences#

    A <Defer> whose content is an AsyncIterable streams one patch per yielded item, until the iterable ends or the client disconnects. Every patch carries the <Defer>'s merge; for a different merge per chunk, use separate <Defer>s.

    import { Defer } from "@vincle/flow";
    declare function fetchRows(page: number): Promise<{ id: number; name: string }[]>;
    function Feed() {
    return (
    <Defer target="feed" merge="append">
    {async function* () {
    let page = 1;
    while (true) {
    const rows = await fetchRows(page++);
    if (rows.length === 0) break;
    for (const row of rows) {
    yield <tr key={row.id}>{row.name}</tr>;
    }
    }
    }}
    </Defer>
    );
    }

    The source need not be a fixed dataset: anything adaptable into an AsyncIterable works, including a live event source. Node's events.on turns an EventEmitter into one; pass it the factory's signal (the request's abort signal combined with the fragment's timeout) so the listener stops when either fires:

    import { Defer } from "@vincle/flow";
    import { on, type EventEmitter } from "node:events";
    declare const priceFeed: EventEmitter; // emits "price" with { symbol, value }
    function LivePrice() {
    return (
    <Defer target="price">
    {async function* (signal) {
    for await (const [price] of on(priceFeed, "price", { signal })) {
    yield (
    <span>
    {price.symbol}: {price.value}
    </span>
    );
    }
    }}
    </Defer>
    );
    }