---
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-11T00:46:00.423Z"
---

With streaming SSR, Vue can send the document shell before an async component calls `useHead()`. Unhead inserts a small script after each resolved Suspense chunk so those later entries still reach the browser's `<head>`. This guide uses Vue's [`renderToWebStream()`](https://vuejs.org/api/ssr.html#rendertowebstream), which returns a Web `ReadableStream`.

## 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.

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

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

<warning>

The automatic wrapper currently emits its server bootstrap and update scripts without nonce attributes, so a nonce-only Content Security Policy will block them. If your renderer exposes reliable chunk boundaries, use the [template-free core flow](/docs/typescript/head/guides/core-concepts/streaming#template-free-integration) to apply the same fresh, per-response nonce to the bootstrap and every update script, as described in [MDN's CSP guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP#nonces). 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 the production server reads the same built `index.html` for every request, 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, but create a new `head` for every request. Sharing a head instance can retain entries and expose one request's tags to another.

In development, prepare the result of `transformIndexHtml()` for each request. Run `prepareTemplate()` again whenever the HTML changes, and never replace an outlet after preparation.

### 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')
})
```

An undefined result means the server response did not initialize the stream queue. Check that the server used the streaming renderer and that its bootstrap script was not removed from the template.

## Usage

Use `useHead()` inside the async component. The wrapper emits its entry after the corresponding Suspense chunk resolves:

```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'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.
