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
| Mode | Value | TTFB | Status Codes | Thrown Responses |
|---|---|---|---|---|
| Wait | false | Higher | Correct | Fully supported |
| Shell | 'shell' | Medium | Correct for shell | Caught in shell, streamed in Suspense |
| Full Stream | true | Lowest | Always 200 | Unreliable 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(),
});