Index of every page on this site: llms.txt
Svelte
Core Concepts

Streaming SSR

Svelte's 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>.
Many bots and link previews do not run patch scripts. Keep discoverable head tags in the Svelte render.

Setup

Vite Plugin

Enable the stream bootstrap and client integration:

// 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().

Pass each response's CSP nonce to createStreamableHead({ nonce }). Unhead applies it to its bootstrap and patch scripts. Use the same nonce in the response CSP header and application scripts. See the core CSP example for request setup.

Server Entry

// 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, nonce?: string) {
  const { head, wrapStream } = createStreamableHead({ nonce })
  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

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

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

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.

Unhead parses raw templates on each request. If production HTML stays unchanged, reuse a prepareTemplate() result. Each request still resolves head tags. Prepared templates do not cache rendered head output.

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

Client Entry

// 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:

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

Did this page help you?