---
title: "Streaming SSR"
description: "Stream head tags as Suspense boundaries resolve during SolidJS SSR"
canonical_url: "https://unhead.unjs.io/docs/solid-js/head/guides/core-concepts/streaming"
last_updated: "2026-06-30T06:54:43.755Z"
---

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

1. **Shell complete** - Solid calls `onCompleteShell`, initial `<head>` tags render
2. **Suspense resolves** - Resources settle and components call `useHead()`
3. **Patches stream** - Unhead injects DOM updates as inline scripts
4. **Client hydrates** - The client head instance picks up the final state

## Setup

### Vite Plugin

The plugin transforms your components to enable streaming head updates:

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

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

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

### Server Entry

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

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

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