GitHubnpm

Streaming SSR

Juice supports three streaming modes for HTML responses. Pick the tradeoff between time-to-first-byte and correctness that suits your app.

Streaming Modes

ModeValueTTFBStatus CodesThrown Responses
WaitfalseHigherCorrectFully supported
Shell'shell'MediumCorrect for shellCaught in shell, streamed in Suspense
Full StreamtrueLowestAlways 200Unreliable in Suspense

Recommended: Shell Streaming

Shell mode waits for the synchronous shell (layouts, synchronous components) to render before sending any bytes. This lets the runtime detect thrown Response objects in the shell and set the correct HTTP status code. Remaining Suspense boundaries stream progressively as they resolve.

createRouter(manifest, {
  streaming: 'shell',
});

Thrown Response Pattern

Throw a Response anywhere in a server component to short-circuit rendering. The runtime catches it and returns it directly as the HTTP response. This works for redirects, 404s, and any other status code.

import { redirect } from '@cmj/juice/runtime';

export default async function Product({ params }: { params: { id: string } }) {
  const product = await db.find(params.id);
  if (!product) {
    throw new Response('Not Found', { status: 404 });
  }
  if (product.archived) {
    redirect('/products');
  }
  return <h1>{product.name}</h1>;
}

Error Recovery with Inline Scripts

When streaming is enabled and an error occurs inside a Suspense boundary after headers have been committed, the runtime injects an inline script that replaces the failed boundary with the error fallback content. This avoids a broken page while maintaining the stream.

loading.tsx and error.tsx

Place a loading.tsx file next to a route to provide a Suspense fallback. Place an error.tsx file for an error boundary fallback. Both are discovered automatically by the compiler.

// app/routes/blog/loading.tsx
export default function BlogLoading() {
  return <div className="skeleton">Loading post...</div>;
}

// app/routes/blog/error.tsx
export default function BlogError({ error }: { error: Error }) {
  return <div className="error">Failed to load post.</div>;
}

Per-Route Boundary Override

Use the response.boundary export to override the global streaming mode for a specific route:

export const response = {
  boundary: 'shell',
};

CSP Nonce

When using streaming, the runtime injects inline scripts for error recovery and CSS bootstrapping. Pass a nonce to createRouter() to make these scripts CSP-compliant.

createRouter(manifest, {
  streaming: 'shell',
  nonce: crypto.randomUUID(),
});