For AI agents: fetch /llms.txt first for the curated documentation index, then use same-host Markdown pages when available.

Concepts

Streaming

Deferred fragments stream a page in two phases on one connection: the shell and loading boundaries flush immediately, then each fragment's completed HTML arrives as soon as its data resolves — out of order, without blocking the others.

One connection, out of order

A route that waits for slow data normally holds the whole document hostage. With deferred streaming, the first chunk on the wire is the complete document skeleton — <head>, layout, and a loading boundary for every deferred fragment. The connection stays open and each fragment streams in when its renderer finishes. A fragment that takes two seconds never delays one that takes fifty milliseconds: the fastest fragment renders first regardless of the order it was deferred.

Deferring a fragment

Define the fragment with a render function and optional loading and error renderers, then call context.defer(fragment) where its content belongs. defer() returns the loading boundary synchronously, so it interpolates straight into the route body.

import { fragment, html, route } from "@nativefragments/core/server";

const relatedPosts = fragment("related-posts", {
  loading: () => html`<p class="skeleton">Finding related posts…</p>`,
  error: () => html`<p role="status">Related posts are unavailable.</p>`,
  render: async (context) => {
    const posts = await loadRelated(context.params.slug, {
      signal: context.signal,
    });
    return html`<ul>
      ${posts.map((post) => html`<li><a href="${post.url}">${post.title}</a></li>`).join("")}
    </ul>`;
  },
});

export const postRoute = route("/posts/:slug", {
  render: (context) => html`<article>
    <h1>…</h1>
    ${context.defer(relatedPosts, { class: "related" })}
  </article>`,
  fragments: [relatedPosts],
});

The second argument spreads attributes onto the boundary element, which carries aria-busy and data-fragment-state while loading. Deferred renderers start the moment defer() collects them, so data fetches overlap with the rest of the route render.

Loading, errors, and timeouts

loading() must be synchronous — it ships in the first chunk. render() and error() may be async. The HTTP status is committed with the first byte, so a fragment that fails mid-stream can never turn the page into a 500: it streams its error() boundary in place and the rest of the page is unaffected.

Every deferred fragment has a timeout (default 15 seconds, configurable per fragment via timeout or per app via the adapter's deferredTimeout option), so a hung data source ends as an error boundary instead of a never-finishing response. Pass context.signal to fetch so timed-out or abandoned requests cancel their network work.

Streaming shells

To stream, the adapter needs to know where the route body sits inside your shell. A shell that returns { before, after } when called without a body makes the split explicit:

import { html } from "@nativefragments/core/server";

export const shell = ({ body, meta, nonce }) => {
  const parts = {
    before: html`<!doctype html>
<html lang="en">
  <head><title>${meta.title}</title></head>
  <body><main id="content-slot">`,
    after: html`</main></body></html>`,
  };
  if (body === undefined) return parts;
  return html`${parts.before}${body}${parts.after}`;
};

Plain string shells keep working — the adapter locates the body automatically. If it cannot (for example the shell escapes body), the response falls back to buffered rendering and a warning is logged, so streaming never fails silently.

Fragment navigation streams too

Client-side navigation requests (the x-fragment: true path) use the same deferred work. The browser router replaces the target with the route body and its loading boundaries as soon as the first HTML frame arrives, then reveals each completed fragment from later frames on that same response. The shell stays mounted, links and history keep their normal fragment-navigation behavior, and one slow region does not hold the rest of the navigation hostage.

Streaming is negotiated with X-NativeFragments-Protocol: 1. Compatible responses echo the version and include a private frame token. Requests without a supported version receive completed buffered HTML. This makes stale browser tabs safe across protocol changes.

Prefetching still ends in completed HTML in the fragment cache. If a prefetched route has finished before it is opened, navigation applies it immediately; otherwise a user-initiated request streams normally.

Crawlers and SEO

Streamed fragments arrive as real, parseable HTML in the same response the crawler fetched — not inert templates and not client-side fetches. Stream order is invisible to anything that reads the final document. On document loads, a single delegated script (carrying the adapter's CSP nonce) moves each fragment into its boundary. During client-side navigation, the browser router performs the same reveal as HTML frames arrive. Crawlability is determined by what is in the response, and the full content is in the response.

See also