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 content resolves, updating the <head> in real-time.
<head> tags render with the document shelluseHead()The plugin transforms your components to enable streaming head updates:
// vite.config.ts
import { unheadSveltePlugin } from '@unhead/svelte/stream/vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
svelte(),
unheadSveltePlugin(),
],
})
// entry-server.ts
import { render as _render } from 'svelte/server'
import { createStreamableHead, UnheadContextKey } from '@unhead/svelte/stream/server'
import App from './App.svelte'
export async function render(url: string, template: string) {
const { head, wrapStream } = createStreamableHead()
const context = new Map()
context.set(UnheadContextKey, head)
const rendered = await _render(App, {
props: { url },
context,
})
const svelteStream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(rendered.body))
controller.close()
},
})
return wrapStream(svelteStream, template)
}
// entry-client.ts
import { mount } from 'svelte'
import { createStreamableHead, UnheadContextKey } from '@unhead/svelte/stream/client'
import App from './App.svelte'
const head = createStreamableHead()
const context = new Map()
context.set(UnheadContextKey, head)
mount(App, {
target: document.getElementById('app')!,
context,
})
Use useHead() normally in your components. Tags stream automatically as async content resolves:
<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>
If your Svelte SSR is fully synchronous, stick with standard SSR. The streaming setup adds complexity for no benefit when all head tags are available at initial render.