Streaming SSR
Standard SSR waits for everything to render before sending HTML. Streaming sends the document shell immediately, then streams content as Suspense boundaries resolve.
The problem: resources inside <Suspense> 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 each Suspense boundary resolves, updating the <head> in real-time.
How It Works
- Shell complete - Solid calls
onCompleteShell, initial<head>tags render - Suspense resolves - Resources settle and components call
useHead() - Patches stream - Unhead injects DOM updates as inline scripts
- Client hydrates - The client head instance picks up the final state
Setup
Vite Plugin
The plugin transforms your components to enable streaming head updates:
// vite.config.ts
import { unheadSolidPlugin } from '@unhead/solid-js/stream/vite'
import solid from 'vite-plugin-solid'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
solid(),
unheadSolidPlugin(),
],
})
Server Entry
// entry-server.tsx
import { renderToStream } from 'solid-js/web'
import { createStreamableHead, UnheadContext } from '@unhead/solid-js/stream/server'
import App from './App'
export function render(url: string, template: string) {
const { head, onCompleteShell, wrapStream } = createStreamableHead()
const { readable, writable } = new TransformStream()
renderToStream(() => (
<UnheadContext.Provider value={head}>
<App url={url} />
</UnheadContext.Provider>
), { onCompleteShell }).pipeTo(writable)
return wrapStream(readable, template)
}
Client Entry
// entry-client.tsx
import { hydrate } from 'solid-js/web'
import { createStreamableHead, UnheadContext } from '@unhead/solid-js/stream/client'
import App from './App'
const head = createStreamableHead()
hydrate(() => (
<UnheadContext.Provider value={head}>
<App url={window.location.pathname} />
</UnheadContext.Provider>
), document.getElementById('app')!)
Usage
Use useHead() normally in your components. Tags from resources inside Suspense boundaries stream automatically:
import { Suspense, createResource } from 'solid-js'
import { useHead } from '@unhead/solid-js'
function AsyncPage() {
const [data] = createResource(fetchPageData)
useHead({
title: () => data()?.title,
meta: [
{ name: 'description', content: () => data()?.description }
]
})
return <div>{data()?.content}</div>
}
function App() {
return (
<Suspense fallback={<Loading />}>
<AsyncPage />
</Suspense>
)
}
When to Skip
If you're not using Suspense with resources, stick with standard SSR. The streaming setup adds complexity for no benefit when all head tags are synchronous.