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 extensionvike-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.
// server.jsimport { 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.jsimport { 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().
pageContext promise: You can return a pageContext promise, in order to send initial data to the client after the stream ends.
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.jsexport { 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.jsexport { 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.jsconst pageContext = await renderPage(pageContextInit)const { httpResponse } = pageContext// Using pageContext.httpResponse.pipe() as usualhttpResponse.pipe(res)
If your server expects a readable stream (e.g. Cloudflare Workers) you can use new TransformStream():
// worker.jsconst { 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.jsexport { 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 } = httpResponseres.send(body)*/// But this works:const body = await httpResponse.getBody()assert(typeof body === 'string')res.send(body)
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.jsexport { 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 already hydrated).
Instead of using HTML streaming, an easy alternative is to use a stateful component. But the issue is that the content that is rendered later isn't rendered to HTML. For example, a product page with content fetched from a database won't get the SEO nor performance advantages of SSR.
With HTML streaming, all content (including the content that is rendered later) is rendered to HTML.
Vike has first-class support for HTML streaming and progressive rendering.