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

Unhead can wrap a Svelte SSR response in a streaming document shell. Svelte's [`render()` server API](https://svelte.dev/docs/svelte/svelte-server#render) returns the completed `body` and `head`, so the adapter captures head entries during that render before streaming the completed body.

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

## Setup

### Vite Plugin

The plugin installs the streaming bootstrap and client integration. It does not make Svelte's synchronous server renderer incremental:

```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 currently emits its server bootstrap script without a nonce attribute, so a nonce-only Content Security Policy will block it. Use the [template-free core flow](/docs/typescript/head/guides/core-concepts/streaming#template-free-integration) to apply a fresh, per-response nonce as described in [MDN's CSP guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP#nonces), or 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 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/svelte/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 { 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,
})
```

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()` normally in components rendered into the response:

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

If you render Svelte to a complete string before sending the response, use standard SSR. Wrapping that completed output in a stream does not make Svelte's render itself asynchronous.
