Server-side rendering library for Vue.js applications with support for streaming and multiple environments
—
Modern Web Streams API support for serverless environments, edge computing platforms, and modern web standards. These functions enable Vue SSR in environments like CloudFlare Workers, Vercel Edge Functions, and other platforms that support Web Streams instead of Node.js streams.
Creates a Web ReadableStream that outputs the rendered HTML. This function provides native Web Streams API integration for modern web platforms and edge environments.
/**
* Renders input as a Web ReadableStream
* @param input - Vue application instance or VNode to render
* @param context - Optional SSR context for teleports and additional data
* @returns Web ReadableStream containing the rendered HTML
* @throws Error if ReadableStream constructor is not available in global scope
*/
function renderToWebStream(
input: App | VNode,
context?: SSRContext
): ReadableStream;Usage Examples:
import { createSSRApp } from "vue";
import { renderToWebStream } from "@vue/server-renderer";
const app = createSSRApp({
template: `
<div>
<h1>Web Streams</h1>
<p>Content streamed via Web ReadableStream</p>
</div>
`,
});
// Direct usage with Web Response
const stream = renderToWebStream(app);
const response = new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
// CloudFlare Workers example
export default {
fetch(request: Request): Response {
const vueApp = createSSRApp({
template: `<div>Hello from CloudFlare Workers!</div>`
});
const stream = renderToWebStream(vueApp);
return new Response(stream, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'public, max-age=300'
}
});
}
};
// Vercel Edge Function example
export const config = { runtime: 'edge' };
export default function handler(request: Request): Response {
const vueApp = createSSRApp({
template: `<div>Hello from Vercel Edge!</div>`
});
return new Response(renderToWebStream(vueApp), {
headers: { 'Content-Type': 'text/html' }
});
}Pipes the rendered output directly to an existing Web WritableStream. This provides more control over the destination and allows for custom stream processing.
/**
* Render and pipe to an existing Web WritableStream instance
* @param input - Vue application instance or VNode to render
* @param context - Optional SSR context for teleports and additional data
* @param writable - Web WritableStream to pipe the output to
*/
function pipeToWebWritable(
input: App | VNode,
context?: SSRContext,
writable: WritableStream
): void;Usage Examples:
import { createSSRApp } from "vue";
import { pipeToWebWritable } from "@vue/server-renderer";
const app = createSSRApp({
template: `
<div>
<h1>Piped Web Stream</h1>
<p>This content is piped to a WritableStream</p>
</div>
`,
});
// Using TransformStream for processing
const { readable, writable } = new TransformStream();
// Pipe Vue content to the writable side
pipeToWebWritable(app, {}, writable);
// Process the readable side
const response = new Response(readable, {
headers: { 'Content-Type': 'text/html' }
});
// Custom WritableStream implementation
class CustomWriter {
constructor() {
this.chunks = [];
}
write(chunk: Uint8Array): Promise<void> {
this.chunks.push(chunk);
return Promise.resolve();
}
close(): Promise<void> {
const content = new TextDecoder().decode(
new Uint8Array(this.chunks.flat())
);
console.log('Final content:', content);
return Promise.resolve();
}
abort(reason: any): Promise<void> {
console.error('Stream aborted:', reason);
return Promise.resolve();
}
}
const customWritable = new WritableStream(new CustomWriter());
pipeToWebWritable(app, {}, customWritable);Both functions support SSR context for teleports and custom data:
import { createSSRApp } from "vue";
import { renderToWebStream } from "@vue/server-renderer";
const app = createSSRApp({
template: `
<div>
<h1>Main Content</h1>
<Teleport to="#modal">
<div class="modal">Modal content</div>
</Teleport>
</div>
`,
});
// CloudFlare Workers with teleports
export default {
async fetch(request: Request): Promise<Response> {
const context = {
url: request.url,
userAgent: request.headers.get('User-Agent'),
};
const { readable, writable } = new TransformStream();
// Start rendering (non-blocking)
pipeToWebWritable(app, context, writable);
// Create full HTML response with teleports
const reader = readable.getReader();
const encoder = new TextEncoder();
return new Response(
new ReadableStream({
async start(controller) {
// Send HTML document start
controller.enqueue(encoder.encode('<!DOCTYPE html><html><body>'));
// Stream main content
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
controller.enqueue(value);
}
// Add teleported content
if (context.teleports) {
for (const [target, content] of Object.entries(context.teleports)) {
const teleportHtml = `<div id="${target.slice(1)}">${content}</div>`;
controller.enqueue(encoder.encode(teleportHtml));
}
}
// Close HTML document
controller.enqueue(encoder.encode('</body></html>'));
controller.close();
} catch (error) {
controller.error(error);
}
}
}),
{
headers: { 'Content-Type': 'text/html' }
}
);
}
};// worker.ts
import { createSSRApp } from "vue";
import { renderToWebStream } from "@vue/server-renderer";
export default {
async fetch(request: Request, env: any, ctx: any): Promise<Response> {
// Parse URL for routing
const url = new URL(request.url);
const app = createSSRApp({
setup() {
return { path: url.pathname };
},
template: `
<div>
<h1>CloudFlare Workers + Vue SSR</h1>
<p>Current path: {{ path }}</p>
<p>Rendered at: ${new Date().toISOString()}</p>
</div>
`
});
const stream = renderToWebStream(app, {
requestUrl: request.url,
timestamp: Date.now()
});
return new Response(stream, {
headers: {
'Content-Type': 'text/html',
'X-Powered-By': 'CloudFlare Workers + Vue'
}
});
}
};// pages/api/ssr.ts
import { createSSRApp } from "vue";
import { renderToWebStream } from "@vue/server-renderer";
export const config = {
runtime: 'edge',
};
export default async function handler(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const name = searchParams.get('name') || 'World';
const app = createSSRApp({
setup() {
return { name };
},
template: `
<div>
<h1>Hello {{ name }}!</h1>
<p>Rendered with Vercel Edge Functions</p>
</div>
`
});
const stream = renderToWebStream(app);
return new Response(stream, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'public, s-maxage=60'
}
});
}// main.ts
import { createSSRApp } from "vue";
import { renderToWebStream } from "@vue/server-renderer";
Deno.serve(async (request: Request): Promise<Response> => {
const app = createSSRApp({
template: `
<div>
<h1>Deno Deploy + Vue SSR</h1>
<p>Fast edge rendering with Deno</p>
</div>
`
});
const stream = renderToWebStream(app);
return new Response(stream, {
headers: {
'Content-Type': 'text/html',
'X-Powered-By': 'Deno Deploy'
}
});
});import { renderToWebStream } from "@vue/server-renderer";
class HTMLWrapperTransform {
constructor() {
this.headerSent = false;
}
transform(chunk: Uint8Array, controller: TransformStreamDefaultController) {
if (!this.headerSent) {
// Add HTML document wrapper
const header = new TextEncoder().encode(
'<!DOCTYPE html><html><head><title>My App</title></head><body>'
);
controller.enqueue(header);
this.headerSent = true;
}
controller.enqueue(chunk);
}
flush(controller: TransformStreamDefaultController) {
// Close HTML document
const footer = new TextEncoder().encode('</body></html>');
controller.enqueue(footer);
}
}
const app = createSSRApp(/* your app */);
const vueStream = renderToWebStream(app);
const { readable, writable } = new TransformStream(new HTMLWrapperTransform());
// Pipe Vue stream through transformer
vueStream.pipeTo(writable);
// Return processed stream
return new Response(readable, {
headers: { 'Content-Type': 'text/html' }
});import { renderToWebStream } from "@vue/server-renderer";
export default {
async fetch(request: Request): Promise<Response> {
try {
const app = createSSRApp({
setup() {
// Simulate potential error
const shouldError = new URL(request.url).searchParams.has('error');
if (shouldError) {
throw new Error('Component error');
}
return {};
},
template: '<div>Success!</div>'
});
const stream = renderToWebStream(app);
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
} catch (error) {
// Return error page
const errorStream = renderToWebStream(createSSRApp({
template: `<div>Error: ${error.message}</div>`
}));
return new Response(errorStream, {
status: 500,
headers: { 'Content-Type': 'text/html' }
});
}
}
};import { renderToWebStream } from "@vue/server-renderer";
class PerformanceMonitoringTransform {
constructor() {
this.startTime = Date.now();
this.chunkCount = 0;
}
transform(chunk: Uint8Array, controller: TransformStreamDefaultController) {
this.chunkCount++;
if (this.chunkCount === 1) {
const ttfb = Date.now() - this.startTime;
console.log(`Time to first byte: ${ttfb}ms`);
}
controller.enqueue(chunk);
}
flush(controller: TransformStreamDefaultController) {
const totalTime = Date.now() - this.startTime;
console.log(`Total render time: ${totalTime}ms, chunks: ${this.chunkCount}`);
}
}
const app = createSSRApp(/* your app */);
const vueStream = renderToWebStream(app);
const { readable, writable } = new TransformStream(new PerformanceMonitoringTransform());
vueStream.pipeTo(writable);
return new Response(readable);Web Streams are supported in:
--experimental-web-streams flag)Web Streams provide excellent memory characteristics:
Web Streams include robust error handling:
Install with Tessl CLI
npx tessl i tessl/npm-vue--server-renderer