HTML Streaming

HTML Streaming support is a beta feature; breaking changes may be introduced in minor version updates.

Basics

// renderer/_deault.page.server.js

export { render }

import { escapeInject } from 'vite-plugin-ssr'
import { renderToStream } from 'some-view-framework' // React, Vue, ...

async function render(pageContext) {
  const { Page } = pageContext

  const stream = renderToStream(Page)

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`
}
// server.js

app.get("*", async (req, res, next) => {
  const pageContextInit = { url: req.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) return next()

  // If `renderToStream()` returns a Node.js Stream:
  const stream = await httpResponse.getNodeStream()
  // If `renderToStream()` returns a Web Stream:
  const stream = await httpResponse.getWebStream()

  stream.pipe(res)
})

Examples & docs

Examples:

Stream Docs & API:

Stream to string

We can convert the stream to a string:

  // If we provide a stream into our `render()` hook's `escapeInject`, we can
  // still get a string.

  /* This won't work: (a stream cannot be consumed synchronously)
  const { body } = httpResponse
  res.send(body)
  */

  // But we can do:
  const body = await httpResponse.getBody()
  assert(typeof body === 'string')
  res.send(body)

Stream pipes

We can also use Stream pipes.

// renderer/_deault.page.server.js

export { render }

import { escapeInject, pipeNodeStream, pipeWebStream } from 'vite-plugin-ssr'
import { pipeToWritable } from 'some-view-framework' // React, Vue, ...

async function render(pageContext) {
  const { Page } = pageContext

  // Node.js Stream
  const streamPipe = pipeNodeStream(writable => {
    // `writable` is a Node.js writable
    pipeToWritable(Page, writable)
  })
  // Web Stream
  const streamPipe = pipeWebStream(writable => {
    // `writable` is a Web writable
    pipeToWritable(Page, writable)
  })

  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${streamPipe}</div>
      </body>
    </html>`
}
// server.js

app.get("*", async (req, res, next) => {
  const pageContextInit = { url: req.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) return next()

  // Node.js Stream
  httpResponse.pipeToNodeWritable(res)
  // Web Stream
  httpResponse.pipeToWebWritable(res)
})

Initial data after streaming

Some data fetching tools, such as Relay, provide the initial data only after the stream as ended.

In such situations, we can return a pageContext promise in our render() hook:

// renderer/_deault.page.server.js

export { render }
export { passToClient }

import { escapeInject } from 'vite-plugin-ssr'
import { renderToStream } from 'some-view-framework' // React, Vue, ...

const passToClient = ['initialData']

async function render(pageContext) {
  const { Page } = pageContext

  const stream = renderToStream(Page)

  const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`

  const pageContextPromise = (async () => {
     return {
       // Some `initialData` provided after the stream has ended
       initialData,
     }
  })()

  return {
    documentHtml,
    pageContext: pageContextPromise
  }
}