---
title: Streaming
---

# 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](/integration/static) 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.

```tsx
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()`](/integration/http-serving), 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](/integration/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.

```tsx
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:

```tsx
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>
  );
}
```
