---
title: "Streaming SSR"
description: "Wrap a Svelte SSR response with Unhead's streaming document shell"
canonical_url: "https://unhead.unjs.io/docs/svelte/head/guides/core-concepts/streaming"
last_updated: "2026-08-31T02:57:54.537Z"
---

Svelte's [`render()`](https://svelte.dev/docs/svelte/svelte-server#render) returns a completed body and head.
Unhead can wrap that result in a Web stream, but Svelte does not render it in chunks.

## How It Works

1. **Render:** Svelte produces the complete body and initial head entries.
2. **Shell:** Unhead writes those entries into the document template.
3. **Response:** The completed body is delivered through a Web stream.
4. **Hydration:** The client instance takes over the initial state.

## Where Tags Render

Svelte completes every component before `render()` returns.
Component calls to `useHead()` therefore reach the initial document shell.

The streaming option does not make `{#await}` blocks resolve in later server chunks.
If server code adds a head entry after the shell, Unhead handles it as a late entry:

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

<warning>

Many bots and link previews do not run patch scripts.
Keep discoverable head tags in the Svelte render.

</warning>

## Setup

### Vite Plugin

Enable the stream bootstrap and client integration:

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

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

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

<warning>

The automatic wrapper does not add a CSP nonce to its bootstrap script.
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 { render as _render } from 'svelte/server'
import { createStreamableHead, type PreparedTemplate, UnheadContextKey } from '@unhead/svelte/stream/server'
import App from './App.svelte'

export async function render(url: string, template: string | PreparedTemplate) {
  const { head, wrapStream } = createStreamableHead()
  const context = new Map()
  context.set(UnheadContextKey, head)

  const rendered = _render(App, {
    props: { url },
    context,
  })

  const svelteStream = new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode(rendered.body))
      controller.close()
    },
  })

  return wrapStream(svelteStream, 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/svelte/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 { hydrate } from 'svelte'
import { createStreamableHead, UnheadContextKey } from '@unhead/svelte/stream/client'
import App from './App.svelte'

const head = createStreamableHead()
if (!head)
  throw new Error('Unhead streaming bootstrap state is missing')

const context = new Map()
context.set(UnheadContextKey, head)

hydrate(App, {
  target: document.getElementById('app')!,
  context,
})
```

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

## Usage

Call `useHead()` during the Svelte render:

```svelte
<script lang="ts">
import { useHead } from '@unhead/svelte'

const { data } = $props()

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

<div>{data.content}</div>
```

## When to Skip

Use standard SSR unless another response stage requires a Web stream.
