Docs/Core Concepts/Streaming
GitHub

Streaming

Streaming lets the server send the page shell immediately and deliver the real content right after — without waiting for slow data sources (databases, remote APIs, LLMs) to finish before the first byte.

# The pattern

  1. The server renders the loading shell from loading.ts and sends it immediately
  2. The client sees paintable UI in one round trip
  3. The shell loads a module script that fetches /__nix-js/render?page=<current-path>
  4. When the data is ready, #app is replaced with the real page and nix-js:rendered fires (islands re-hydrate)

This is enabled automatically whenever a route has a loading.ts file.

# Creating a loading state

// src/app/blog/[slug]/loading.ts
import { html } from "@deijose/nix-js";

export default function BlogPostLoading() {
  return html`
    <div class="skeleton" aria-busy="true">
      <div class="skeleton-title"></div>
      <div class="skeleton-line"></div>
      <div class="skeleton-line"></div>
    </div>
  `;
}

The loading component renders inside a #nix-js-loading placeholder that the shell swaps out. While the real content is being fetched, the shell title is Loading....

# Full example

src/app/blog/[slug]/
├── page.ts
├── page.data.ts
└── loading.ts
// src/app/blog/[slug]/page.data.ts
import type { PageDataLoad } from "@deijose/nix-js-kit";

export const load: PageDataLoad = async ({ params }) => {
  // Simulates a slow database query (e.g. 500ms)
  const post = await fetchPost(params.slug);
  return { post };
};

async function fetchPost(slug: string | string[]) {
  await new Promise((r) => setTimeout(r, 500));
  return { title: `Post ${slug}`, content: "..." };
}
// src/app/blog/[slug]/page.ts
import { html } from "@deijose/nix-js";
import type { PageProps } from "@deijose/nix-js-kit";
import { load } from "./page.data";

export default function BlogPost({ data }: PageProps<typeof load>) {
  return html`
    <article>
      <h1>${data.post.title}</h1>
      <p>${data.post.content}</p>
    </article>
  `;
}

With the loading.ts file in place, the shell paints instantly and the article appears as soon as fetchPost resolves — the user sees a skeleton instead of a blank page or a spinner on a frozen request.

# Configuration

Option Where Effect
loading.ts presence Route segment Enables streaming for that route
streaming: false createSsrServer options Disables streaming globally (plain render)
--cache-dir / --default-revalidate nix-js-kit start ISR caching also applies to the /__nix-js/render endpoint
ℹ Note

Streaming is shell + client fetch — the shell is sent immediately, and the content arrives through the render endpoint. True out-of-order server-side flushing (writing chunks as each async boundary resolves) is on the roadmap; streamBoundary below exists as the primitive for it.

# streamBoundary — async content placeholder

streamBoundary is an experimental primitive for content that arrives after mount:

import { html, streamBoundary } from "@deijose/nix-js-kit";

export default function Widget() {
  const posts = fetchRecentPosts();

  return html`
    ${streamBoundary({
      fallback: html`<div class="skeleton">Loading posts...</div>`,
      promise: posts,
      children: (posts) => html`
        <ul>${posts.map((p) => html`<li>${p.title}</li>`)}</ul>
      `,
    })}
  `;
}

On the client it mounts the fallback immediately and swaps in children(value) when the promise resolves. For SSR/SSG it renders only the fallback.

# Programmatic streaming

The renderStreamingPage function drives the shell-and-fetch flow programmatically — see SSR API for the signature.

# When to use streaming

  • Slow data sources — databases, third-party APIs, AI completions
  • Per-route shells — the loading UI differs per route
  • SSR deployments — only meaningful when rendering happens on request (start, adapters, Vercel/Netlify functions)

For fully static sites, loading.ts has no effect — generateStaticParams + SSG already produces the finished HTML.

✎ Edit this page on GitHub