⚠️ The
vite-plugin-ssr
project has been renamed
Vike.
- If you are already using vite-plugin-ssr then migrate to Vike.
- For new projects, don't use vite-plugin-ssr but use Vike instead.
HTML Streaming
Examples & docs
React Examples:
Vue Examples:
Stream Docs & API:
Basics
// renderer/_deault.page.server.js
export { render }
import { escapeInject } from 'vite-plugin-ssr/server'
import { renderToStream } from 'some-ui-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>`
}
Node.js platforms (Vercel, AWS EC2, AWS Lambda, ...):
// server.js
import { renderPage } from 'vite-plugin-ssr/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 'vite-plugin-ssr/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 for Web Streams
const readable = httpResponse.getReadableWebStream()
const { statusCode: status, headers } = httpResponse
return new Response(readable, { headers, status })
}
}
pageContext.enableEagerStreaming
By default, the HTML template (we provide in our render()
hook) isn't immediately written to the stream: instead, vite-plugin-ssr
awaits for our UI framework to start writing to the stream.
import { renderToStream } from 'some-ui-framework' // React, Vue, ...
async function render(pageContext) {
const { Page } = pageContext
const stream = renderToStream(Page)
// The HTML template (e.g. `<title>`) isn't immediately written to the stream.
// Instead, vite-plugin-ssr awaits for `stream` to start.
return escapeInject`<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<div id="page-view">${stream}</div>
</body>
</html>`
}
If we set pageContext.enableEagerStreaming
to true
then vite-plugin-ssr
starts writing the HTML template right away.
async function render(pageContext) {
// The HTML template (e.g. `<title>`) is immediately written to the stream
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
}
}
}
Stream to string
We can convert the stream to 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
To be able to use stream pipes, we need to use stampPipe()
:
// renderer/_deault.page.server.js
export { render }
import { renderToStreamPipe } from 'some-ui-framework' // React, Vue, ...
import { escapeInject, stampPipe } from 'vite-plugin-ssr/server'
async function render(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
// We can use `httpResponse.pipe()` as usual
httpResponse.pipe(res)
For Edge platforms that need a readable stream, such as Cloudflare Workers, we 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, we need a pipe wrapper:
// renderer/_deault.page.server.js
import { pipePageToWritable } from 'some-ui-framework'
import { stampPipe, escapeInject } from 'vite-plugin-ssr/server'
export function render(pageContext) {
const { Page } = pageContext
// We use a pipe wrapper so that `pipePageToWritable()` can access `Page`
const pipeWrapper = (writable) => {
pipePageToWritable(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()
, and using new TransformStream()
for Cloudflare Workers.
Initial data after stream end
Some data fetching tools, such as Relay and Vue's onServerPrefetch()
, collect data during the stream.
Consequently, we can determine the initial data (passed to the browser) only after the stream has ended.
In such situations, we can return a pageContext
async function in our render()
hook:
// renderer/_deault.page.server.js
export { render }
export { passToClient }
import { escapeInject } from 'vite-plugin-ssr/server'
import { renderToStream } from 'some-ui-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 () => {
// I'm called after the stream has ended
return {
initialData,
}
}
return {
documentHtml,
pageContext: pageContextPromise
}
}