---
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-08-31T02:57:24.848Z"
---

Vue can send the document shell while async components are still rendering.
Unhead sends their later head entries with the Vue stream.

This guide uses Vue's [`renderToWebStream()`](https://vuejs.org/api/ssr.html#rendertowebstream).

## How It Works

1. **Shell:** Initial tags render with the document shell.
2. **Suspense:** An async component resolves and calls `useHead()`.
3. **Patch:** Unhead writes its head update as an inline script.
4. **Hydration:** The client instance takes over the streamed state.

## Where Tags Render

Create the Vue stream before calling `wrapStream()`. Vue runs its synchronous pass during `renderToWebStream()`.

A `useHead()` call reaches the initial head when both conditions are true:

- It runs before its component's first `await`.
- Every ancestor remains synchronous.

An async ancestor defers its whole subtree. A `<Suspense>` boundary does not change this cutoff.

Later head-only tags become inline patch scripts.
Late JSON-LD, `noscript`, and body-positioned tags become Streamed Body Tags before `</body>`.

<warning>

Many bots and link previews do not run patch scripts.
Register canonical links, robots, descriptions, and Open Graph tags before the shell.

Google can read [JSON-LD from the head or body](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats).

</warning>

## Setup

### Vite Plugin

Enable the `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 }),
  ],
})
```

For webpack projects, import `Unhead` from `@unhead/vue/bundler` and call `Unhead({ streaming: true }).webpack()`.

<warning>

The automatic wrapper does not add CSP nonces to its inline scripts.
For a nonce-only policy, use the [manual core flow](/docs/typescript/head/guides/core-concepts/streaming#template-free-integration).
Otherwise, use standard SSR.

</warning>

### Server Entry

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

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

  app.use(head)

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

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

### Reuse a Production Template

<note>

`prepareTemplate()` and `PreparedTemplate` are experimental. Their API may change in a future minor release.

</note>

If `index.html` stays unchanged, prepare it once before calling `render()`:

```ts
import { readFile } from 'node:fs/promises'
import { prepareTemplate } from '@unhead/vue/stream/server'

const template = prepareTemplate(await readFile('dist/client/index.html', 'utf8'))
```

Keep `template` at process scope. Create a new `head` for every request.
Sharing `head` can expose one request's tags to another.

In development, prepare the result of `transformIndexHtml()` for each request.
Prepare again after any HTML change.

### Client Entry

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

const { app, router } = createApp()
const head = createStreamableHead()
if (!head)
  throw new Error('Unhead streaming bootstrap state is missing')

app.use(head)

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

If this returns `undefined`, the response has no stream bootstrap.
Check the server renderer and HTML template.

## Usage

Call `useHead()` inside the async component:

```vue
<script setup lang="ts">
const data = await fetch('/api/page').then(response => response.json())

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

## When to Skip

If you have no async components, use standard SSR.
