---
title: "Streaming SSR"
description: "Stream head tags as async content resolves during server-side rendering"
canonical_url: "https://unhead.unjs.io/docs/typescript/head/guides/core-concepts/streaming"
last_updated: "2026-06-04T17:36:33.834Z"
---

Standard SSR waits for everything to render before sending HTML. Streaming sends the document shell immediately, then streams content as async operations complete.

The problem: when content loads asynchronously during SSR, any head tags set *after* the initial render never reach the client's `<head>`. The shell has already been sent.

Unhead's streaming integration solves this by injecting `<script>` patches into the stream as content resolves, updating the `<head>` in real-time.

## How It Works

1. **Shell renders** - Initial `<head>` tags render with the document shell
2. **Async content resolves** - New head tags are registered via `head.push()`
3. **Patches stream** - Unhead injects DOM updates as inline scripts
4. **Client hydrates** - The client head instance picks up the final state

## Setup

### Server

Use `wrapStream` to inject head updates into your response stream:

```ts
import { createStreamableHead, wrapStream } from 'unhead/stream/server'

const { head } = createStreamableHead()

head.push({
  title: 'My App',
  htmlAttrs: { lang: 'en' }
})

const template = '<!DOCTYPE html><html><head></head><body></body></html>'

// Wrap your app stream with head injection
const fullStream = wrapStream(head, appStream, template)
return new Response(fullStream)
```

### Client

The client automatically processes queued entries from the stream:

```ts
import { createStreamableHead } from 'unhead/stream/client'

const head = createStreamableHead()
```

## Manual Stream Control

For fine-grained control over when head updates are injected, use `renderSSRHeadShell` and `renderSSRHeadSuspenseChunk` directly:

```ts
import { renderSSRHeadShell, renderSSRHeadSuspenseChunk } from 'unhead/stream/server'

// Render initial shell with current head state
res.write(renderSSRHeadShell(head, template))

// After each async chunk, emit head updates
for await (const chunk of appStream) {
  res.write(chunk)
  const update = renderSSRHeadSuspenseChunk(head)
  if (update)
    res.write(`<script>${update}</script>`)
}

res.write('</body></html>')
```

This approach is useful when integrating with custom streaming frameworks or when you need to control exactly when head patches are emitted.

## Template-Free Integration

For frameworks that construct HTML programmatically (like Nuxt) rather than using an HTML template, use `renderShell` and `createBootstrapScript` directly:

```ts
import { createBootstrapScript, renderShell, renderSSRHeadSuspenseChunk } from 'unhead/stream/server'

// Render and consume all current head entries atomically
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = renderShell(head)

// Get the bootstrap script that creates the client-side stream queue
const bootstrap = createBootstrapScript()

// Build shell HTML programmatically
const shell = '<!DOCTYPE html>'
  + `<html${htmlAttrs ? ' ' + htmlAttrs : ''}>`
  + `<head>${bootstrap}${headTags}</head>`
  + `<body${bodyAttrs ? ' ' + bodyAttrs : ''}>`
  + (bodyTagsOpen || '')

res.write(shell)

// Stream app content, injecting head updates between chunks
for await (const chunk of appStream) {
  res.write(chunk)
  const update = renderSSRHeadSuspenseChunk(head)
  if (update)
    res.write(`<script>${update}</script>`)
}

// Render any final head entries (e.g. payload scripts added post-render)
const closing = renderShell(head)
res.write(closing.bodyTags + '</body></html>')
```

## API Reference

### createStreamableHead

Creates a streaming-aware head instance:

```ts
import { createStreamableHead } from 'unhead/stream/server'

const { head, wrapStream } = createStreamableHead(options?: {
  streamKey?: string  // Custom key for stream identification
})
```

### wrapStream

Wraps a `ReadableStream` with head injection:

```ts
import { wrapStream } from 'unhead/stream/server'

const wrappedStream = wrapStream(
  head: Unhead,
  stream: ReadableStream<Uint8Array>,
  template: string
): ReadableStream<Uint8Array>
```

### renderShell

Renders the current head state and clears entries atomically. Use this instead of manually calling `head.render()` followed by `head.entries.clear()`:

```ts
import { renderShell } from 'unhead/stream/server'

const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = renderShell(head)
```

### createBootstrapScript

Generates the inline `<script>` tag that creates the streaming queue on the window object. Must run before any streaming updates:

```ts
import { createBootstrapScript } from 'unhead/stream/server'

const script = createBootstrapScript() // uses default key '__unhead__'
const script2 = createBootstrapScript('myKey') // custom stream key
```

## When to Skip

If your SSR is fully synchronous (no async data fetching during render), stick with standard SSR. The streaming setup adds complexity for no benefit when all head tags are available at initial render.
