---
title: "Streaming SSR"
description: "Stream head tags as async components resolve during Vue SSR"
canonical_url: "https://unhead.unjs.io/docs/vue/head/guides/core-concepts/streaming"
last_updated: "2026-06-18T00:43:06.191Z"
---

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

The problem: async components using `useHead()` set head tags *after* the initial render. Without streaming support, those tags never reach the client's `<head>`.

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

## How It Works

1. **Shell renders** - Initial `<head>` tags render with the document shell
2. **Suspense boundaries resolve** - Async components call `useHead()`
3. **Patches stream** - Unhead injects DOM updates as inline scripts
4. **Client hydrates** - The client head instance picks up the final state

## Setup

### Vite Plugin

Enable streaming via the unified `Unhead` plugin's `streaming` option:

```ts
// vite.config.ts
import { Unhead } from '@unhead/vue/vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue(),
    Unhead({ streaming: true }),
  ],
})
```

A matching `@unhead/vue/webpack` entry is available for webpack projects.

### Server Entry

```ts
// entry-server.ts
import { renderToWebStream } from 'vue/server-renderer'
import { createStreamableHead } from '@unhead/vue/stream/server'
import { VueHeadMixin } from '@unhead/vue'
import { createApp } from './main'

export async function render(url: string, template: string) {
  const { app, router } = createApp()
  const { head, wrapStream } = createStreamableHead()

  app.use(head)
  app.mixin(VueHeadMixin)

  router.push(url)
  await router.isReady()

  const vueStream = renderToWebStream(app)
  return wrapStream(vueStream, template)
}
```

### Client Entry

```ts
// entry-client.ts
import { createStreamableHead } from '@unhead/vue/stream/client'
import { createApp } from './main'

const { app, router } = createApp()
const head = createStreamableHead()

app.use(head)

router.isReady().then(() => {
  app.mount('#app')
})
```

## Usage

Use `useHead()` normally in your components. Tags from async components stream automatically as Suspense boundaries resolve:

```vue
<script setup lang="ts">
const { data } = await useFetch('/api/page')

useHead({
  title: data.value.title,
  meta: [
    { name: 'description', content: data.value.description }
  ]
})
</script>
```

## When to Skip

If you're not using async components with Suspense, stick with standard SSR. The streaming setup adds complexity for no benefit when all head tags are synchronous.
