⚠️ 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
}
}