Skip to content

Streaming SSR ​

Server-side rendering with streaming responses.

Overview ​

Streaming SSR sends HTML to the browser as it's generated, improving Time to First Byte (TTFB) and perceived performance.

Enable Streaming ​

Set the render mode to streaming:

ts
// routes/posts/[id].tsx
export const config = {
  render: 'streaming'
}

Using defer() ​

Mark slow data as deferred:

ts
import { createLoader, defer } from '@ereo/data'

export const loader = createLoader(async ({ params }) => {
  const post = await db.posts.find(params.id)  // Fast

  return {
    post,
    comments: defer(db.comments.findByPost(params.id)),  // Slow
    related: defer(getRelatedPosts(params.id))           // Slow
  }
})

Using Await Component ​

Render deferred data with Suspense:

tsx
import { Await, Suspense } from '@ereo/client'

export default function PostPage({ loaderData }) {
  const { post, comments, related } = loaderData

  return (
    <article>
      {/* Renders immediately */}
      <h1>{post.title}</h1>
      <p>{post.content}</p>

      {/* Streams in when ready */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Await resolve={comments}>
          {(data) => <CommentList comments={data} />}
        </Await>
      </Suspense>

      <Suspense fallback={<RelatedSkeleton />}>
        <Await resolve={related}>
          {(data) => <RelatedPosts posts={data} />}
        </Await>
      </Suspense>
    </article>
  )
}

Streaming Flow ​

┌─────────────────────────────────────────────────────────────┐
│                        Server                               │
├─────────────────────────────────────────────────────────────┤
│  1. Receive request                                         │
│  2. Start streaming HTML shell                              │
│  3. Execute loader (deferred data starts loading)           │
│  4. Stream initial content with suspense fallbacks          │
│  5. As deferred data resolves, stream replacement scripts   │
│  6. Close stream                                            │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        Browser                              │
├─────────────────────────────────────────────────────────────┤
│  1. Receive HTML shell                                      │
│  2. Display content with skeletons                          │
│  3. Receive replacement scripts                             │
│  4. Replace skeletons with actual content                   │
│  5. Hydrate islands                                         │
└─────────────────────────────────────────────────────────────┘

Error Handling ​

Handle errors in deferred data:

tsx
<Suspense fallback={<Loading />}>
  <Await
    resolve={comments}
    errorElement={<ErrorMessage />}
  >
    {(data) => <CommentList comments={data} />}
  </Await>
</Suspense>

With custom error component:

tsx
function ErrorMessage({ error }) {
  return (
    <div className="text-red-500">
      Failed to load: {error.message}
    </div>
  )
}

Streaming Configuration ​

Configure streaming behavior:

ts
export default defineConfig({
  streaming: {
    // Timeout for deferred data (ms)
    timeout: 10000,

    // Send initial chunk immediately
    flushDelay: 0,

    // Abort on client disconnect
    abortOnClose: true
  }
})

Progressive Enhancement ​

Streaming works without JavaScript:

tsx
<Suspense fallback={<Loading />}>
  <Await resolve={data}>
    {(resolved) => (
      <noscript>
        {/* Content still appears, just not progressively */}
      </noscript>
    )}
  </Await>
</Suspense>

Multiple Suspense Boundaries ​

Nest boundaries for fine-grained loading:

tsx
<Suspense fallback={<PageSkeleton />}>
  <Header />

  <main>
    <Suspense fallback={<ContentSkeleton />}>
      <Await resolve={content}>
        {(data) => <Content data={data} />}
      </Await>
    </Suspense>

    <aside>
      <Suspense fallback={<SidebarSkeleton />}>
        <Await resolve={sidebar}>
          {(data) => <Sidebar data={data} />}
        </Await>
      </Suspense>
    </aside>
  </main>
</Suspense>

When to Use Streaming ​

ScenarioRecommendation
Fast data sourcesRegular SSR
Slow database queriesStreaming + defer
External API callsStreaming + defer
Mixed fast/slow dataStreaming with selective defer
Static pagesSSG

Performance Tips ​

  1. Defer wisely - Only defer genuinely slow operations
  2. Meaningful fallbacks - Use skeletons that match content layout
  3. Error boundaries - Always handle potential failures
  4. Timeouts - Set reasonable timeouts for deferred data

Released under the MIT License.