HTML Streaming

Vike not only has first-class support for HTML streaming (aka SSR streaming), but also provides you with extensive control over the HTML stream.

If you merely want to enable/disable HTML streaming, see the stream setting instead.

Instead of manually integrating HTML Streaming yourself, you can use a UI framework Vike extension vike-react/vike-vue/vike-solid which already integrates HTML Streaming. And you can use Bati to scaffold an app that uses vike-react/vike-vue/vike-solid.

⚠
The documentation on this page is meant for users who want to manually integrate HTML Streaming. If you use vike-react/vike-vue/vike-solid then HTML Streaming is already built-in and you can skip reading this page.

What is HTML Streaming? If you're unfamiliar with HTML streaming then check out Dan Abramov's explanation of SSR, HTML streaming, and Progressive Rendering. (While it explains it in the context of React, we still recommend reading it if you use a UI framework other than React.)

Examples

Examples of manually integrating HTML streaming (without using a Vike extension).

React

Vue

Basics

// /renderer/+onRenderHtml.js
 
export { onRenderHtml }
 
import { escapeInject } from 'vike/server'
import { renderToStream } from 'some-ui-framework' // React, Vue, ...
 
async function onRenderHtml(pageContext) {
  const { Page } = pageContext
 
  const stream = renderToStream(Page)
 
  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`
}

Node.js platforms (Vercel, AWS EC2, AWS Lambda, ...):

// server.js
 
import { renderPage } from 'vike/server'
 
app.get("*", async (req, res, next) => {
  const pageContextInit = { urlOriginal: req.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) return next()
  // httpResponse.pipe() works with Node.js Streams as well as Web Streams
  httpResponse.pipe(res)
})

Edge platforms (e.g. Cloudflare Workers):

// worker.js
 
import { renderPage } from 'vike/server'
 
addEventListener('fetch', (event) => {
  event.respondWith(handleFetchEvent(event))
})
 
async function handleFetchEvent(event) {
  const pageContextInit = { urlOriginal: event.request.url }
  const pageContext = await renderPage(pageContextInit)
  const { httpResponse } = pageContext
  if (!httpResponse) {
    return null
  } else {
    // httpResponse.getReadableWebStream() only works with Web Streams
    const readable = httpResponse.getReadableWebStream()
    const { statusCode: status, headers } = httpResponse
    return new Response(readable, { headers, status })
  }
}

In general, we recommend using pageContext.httpResponse.pipe() because pipe() is able to flush streams. In other words: the stream provided by your UI framework can say "now is a good time to flush the buffer and send it to the user".

As far as we know, only React leverages the flush capability. Thus, the recommendation may be irrelevant if you use a UI framework other than React.

If your server doesn't expose a writable stream then you cannot use pipe(). In that case you can fallback to:

  • pageContext.httpResponse.getReadableWebStream()
  • pageContext.httpResponse.getReadableNodeStream()

For example, Cloudflare Workers doesn't expose any writable stream and the only option is to use getReadableWebStream().

API

enableEagerStreaming

By default, the HTML stream isn't immediately sent to the user. Instead, Vike awaits for your UI framework to start its stream.

// /renderer/+onRenderHtml.js
 
export { onRenderHtml }
 
import { renderToStream } from 'some-ui-framework' // React, Vue, ...
 
async function onRenderHtml(pageContext) {
  const { Page } = pageContext
 
  const stream = renderToStream(Page)
 
  // The HTML template below isn't immediately sent to the user.
  // Instead, Vike awaits for `stream` to start emitting.
  return escapeInject`<!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <div id="page-view">${stream}</div>
      </body>
    </html>`
}

If you set pageContext.enableEagerStreaming to true then Vike starts emitting the HTML template right away.

async function onRenderHtml(pageContext) {
  // The HTML template below is immediately sent to the user.
  const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <head>
        <title>Hello</title>
      </head>
      <body>
        <div id="page-view">${renderToStream(pageContext.Page)}</div>
      </body>
    </html>`
 
  return {
    documentHtml,
    pageContext: {
      enableEagerStreaming: true
    }
  }
}

Make sure your server (or any proxy between your server and the user) doesn't buffer the stream, otherwise you may still notice a delay.

stampPipe()

If your UI framework provides a stream pipe, then you need to use stampPipe().

// /renderer/+onRenderHtml.js
 
export { onRenderHtml }
 
import { renderToStreamPipe } from 'some-ui-framework' // React, Vue, ...
import { escapeInject, stampPipe } from 'vike/server'
 
async function onRenderHtml(pageContext) {
  const { Page } = pageContext
 
  const pipe = renderToStreamPipe(Page)
 
  // If `pipe(writable)` expects `writable` to be a Writable Node.js Stream
  stampPipe(pipe, 'node-stream')
  // If `pipe(writable)` expects `writable` to be a Writable Web Stream
  stampPipe(pipe, 'web-stream')
 
  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${pipe}</div>
      </body>
    </html>`
}

For Node.js:

// server.js
 
const pageContext = await renderPage(pageContextInit)
const { httpResponse } = pageContext
// Using pageContext.httpResponse.pipe() as usual
httpResponse.pipe(res)

If your server expects a readable stream (e.g. Cloudflare Workers) you can use new TransformStream():

// worker.js
 
const { readable, writable } = new TransformStream()
httpResponse.pipe(writable)
const resp = new Response(readable)

For some UI frameworks, such as Vue, you need a pipe wrapper:

// /renderer/+onRenderHtml.js
 
export { onRenderHtml }
 
import { pipePageToWritable } from 'some-ui-framework'
import { stampPipe, escapeInject } from 'vike/server'
 
async function onRenderHtml(pageContext) {
  // Using a pipe wrapper so that pipePageToWritable() can access pageContext.Page
  const pipeWrapper = (writable) => {
    pipePageToWritable(pageContext.Page, writable)
  }
  stampPipe(pipeWrapper, 'node-stream')
 
  return escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div id="page-view">${pipeWrapper}</div>
      </body>
    </html>`
}

See /examples/cloudflare-workers-vue for an example of using a pipe wrapper with Vue's pipeToWebWritable()/pipeToNodeWritable(), as well as using new TransformStream() for Cloudflare Workers.

pageContext.httpResponse.getBody()

You can convert the stream to a string:

/* This won't work: (a stream cannot be consumed synchronously)
const { body } = httpResponse
res.send(body)
*/
 
// But this works:
const body = await httpResponse.getBody()
assert(typeof body === 'string')
res.send(body)

Initial data after stream end

Some data fetching tools, such as Relay and Vue's onServerPrefetch(), collect data during the stream.

Consequently, you can determine the initial data (which needs to be passed to the client-side) only after the stream has ended.

In such situations, you can return a pageContext async function in your onRenderHtml() hook:

// /renderer/+onRenderHtml.js
 
export { onRenderHtml }
 
import { escapeInject } from 'vike/server'
import { renderToStream } from 'some-ui-framework' // React, Vue, ...
 
async function onRenderHtml(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 () => {
     // I'm called after the stream has ended
     return {
       initialData,
     }
  }
 
  return {
    documentHtml,
    pageContext: pageContextPromise
  }
}

Progressive Rendering

Some UI frameworks, such as React, support progressive rendering: while some parts of the UI are being loaded, other parts are already rendered (as well as hydrated!).

Demo:

Instead of using HTML streaming, an alternative and easy way to achieve this is to use a stateful component. But the issue with that approach is that the content that is rendered later isn't rendered to HTML. For example, a product page with content fetched from a database will be missing out on the SEO and performance perks of SSR.

The benefit of using HTML streaming to implement progressive rendering is that all content, even the content that is rendered later, is rendered to HTML.

Vike has first-class support for progressive rendering. (Make sure your UI framework supports progressive rendering.)

Example: vike-react > examples/full/.

See also: Integration > React > React Server Components.

See also