In Vue 3's vue/server-renderer, how do renderToString and the stream renderers differ, and how do you choose between them?
answer
- whole string versus chunks
- Node streams versus web streams
- chunks in document order
- what you can still change
basics
~20 srenderToString resolves to the complete HTML once everything has rendered; the stream renderers push chunks in document order as Node or web streams. Stream for an earlier first byte, use the string to post-process or set the status afterwards.
solid answer
~40 s`renderToString(app, ctx)` returns a `Promise<string>` with the whole app's HTML, after every async part (prefetches, async components) has resolved and teleports have been collected. The streaming family writes chunks as they are produced: `renderToNodeStream` returns a Node `Readable` (not supported in the ESM build, which points you to `pipeToNodeWritable`), `pipeToNodeWritable(app, ctx, writable)` writes into an existing Node writable and ends it, `renderToWebStream` returns a web `ReadableStream` and throws if no global `ReadableStream` exists, and `pipeToWebWritable` writes into a `WritableStream`. Vue streams in document order: when rendering reaches unresolved async work, everything after it waits. Choose the string when you need the full HTML first, to set the status or headers, inject head tags or teleports, or cache it; choose a stream for large pages where sending the top early matters.
code
ts · 21 linesimport { renderToString, pipeToNodeWritable, renderToWebStream } from 'vue/server-renderer'
import type { App } from 'vue'
import type { ServerResponse } from 'node:http'
// whole document: status and headers can still change after rendering
export async function sendString(app: App, res: ServerResponse) {
const html = await renderToString(app, {})
res.statusCode = 200
res.end(`<!DOCTYPE html><html><body><div id="app">${html}</div></body></html>`)
}
// Node stream: chunks go out in document order; the writable is ended for you
export function sendNodeStream(app: App, res: ServerResponse) {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
pipeToNodeWritable(app, {}, res)
}
// Fetch-style runtime: return a web ReadableStream as the response body
export function webResponse(app: App): Response {
return new Response(renderToWebStream(app), { headers: { 'Content-Type': 'text/html' } })
}go deeper
Know that renderToString returns a promise of the whole HTML and that vue/server-renderer also offers Node and web stream variants.
Explain each stream function by runtime and input: returns a stream versus pipes into an existing writable, Node versus web streams, and the ESM-build limitation.
Reason about in-order streaming: where slow components sit, what can no longer change once bytes are out, and how failures truncate a streamed response.
Decide per route whether first-byte time justifies giving up post-render control of status, head and caching, and standardise the choice across the app.
## The APIs at a glance All of these are exported from `vue/server-renderer`, take an app instance (or a vnode) as the first argument, and accept an optional **SSR context** object: | Function | Output | Notes | |---|---|---| | `renderToString(input, context?)` | `Promise<string>` | resolves with the complete HTML | | `renderToNodeStream(input, context?)` | Node `Readable` | not supported in the ESM build; it throws and points to `pipeToNodeWritable` | | `pipeToNodeWritable(input, context, writable)` | writes to a Node `Writable` | calls `writable.end()` when rendering finishes | | `renderToWebStream(input, context?)` | web `ReadableStream` | throws if the global `ReadableStream` constructor is missing | | `pipeToWebWritable(input, context, writable)` | writes to a web `WritableStream` | typically paired with a `TransformStream` | | `renderToSimpleStream(input, context, { push, destroy })` | calls your `push` per chunk | the primitive the others are built on; `push(null)` marks the end | An older `renderToStream` still exists as a deprecated alias that warns and calls `renderToNodeStream`. ## How renderToString behaves `renderToString` renders the tree into an internal buffer, awaits every async part (`onServerPrefetch` callbacks, async components), collects teleported content into `ctx.teleports`, and only then resolves. Consequences: - Nothing can be sent until the slowest part of the page is ready. - You hold the complete HTML, so you can still pick the status code, set headers and cookies, inject head tags recorded in the context, insert teleports anywhere in the shell, or cache the result. - A failure rejects the promise before any byte is written, so you can still send a proper error page. ## How the stream renderers behave The streaming functions all go through `renderToSimpleStream`, which unrolls the same buffer and pushes each string as soon as it is available: 1. Everything up to the first unresolved async part is pushed immediately. 2. At an unresolved part, the stream waits; later siblings may already be rendering, but their HTML is pushed only after it, in **document order**. 3. After the app's HTML, teleports are resolved into `ctx.teleports`, then the end of the stream is signalled. Vue's core renderer does not send out-of-order placeholders that are filled in later; a slow component near the top holds back everything below it. On failure the stream is destroyed (Node) or errored (web `ReadableStream`); `pipeToWebWritable` logs the error and closes the writer. By then the status line and part of the page may already have been sent, so the client receives a truncated document. ## Practical details when streaming - Set headers such as `Content-Type` before the first chunk; once bytes are out they are fixed. - The renderers produce only the app's markup: write the doctype, head and the opening of the mount container yourself before rendering starts, and the closing tags after it ends. - `pipeToNodeWritable` ends the writable when it finishes, so to write a page tail after the app use `renderToSimpleStream` with your own `push`, or, in the CommonJS build, pipe a `renderToNodeStream` result without letting it end the response. - Pass a fresh context object per request, exactly as with `renderToString`. - Measure what changes: streaming improves time to first byte, not total render time, and it can hide a slow component rather than fix it. ## Choosing **Prefer `renderToString` when:** - the page is small or its data is fast, so there is little to gain from streaming; - you must decide the status code or redirects after rendering, for example a not-found detected deep in the tree; - you post-process the HTML: head tags gathered in the SSR context, teleports placed before the app root, full-page caching. **Prefer a stream when:** - pages are large and the top of the document is ready well before the rest; - the first byte matters and the shell and head can be written before rendering starts. **Then pick the flavour by runtime:** Node `http`-style responses use `pipeToNodeWritable` (or `renderToNodeStream` in the CommonJS build); runtimes built on the Fetch API return `new Response(renderToWebStream(app))`, or use `pipeToWebWritable` where `ReadableStream` is not global.
- A Vue component near the top of a streamed page awaits a slow request in onServerPrefetch. What does the browser see?Everything before that component arrives immediately, then the stream pauses: Vue pushes HTML in document order, so the slow component's markup and everything after it wait, even if later siblings finished rendering. Move the slow part lower on the page, fetch it on the client, or accept the delay; Vue core does not reorder chunks.
- Why does importing renderToNodeStream from Vue's ESM server-renderer build fail at runtime?The ESM build is decoupled from Node.js, so it cannot create a Node `Readable` itself. Calling `renderToNodeStream` there throws an error that tells you to use `pipeToNodeWritable` with an existing Node `Writable`, such as the HTTP response, instead.
renderToString prints the whole letter before posting it. Streaming faxes it page by page in order: the reader starts early, but a page still waiting on a figure holds up every page behind it, and a sent page cannot be taken back.
saying these in an interview costs you the question
- Vue's stream renderers send slow components later, out of order, like placeholders.
- renderToString starts sending HTML to the client before rendering finishes.
- With a stream you can still change the status code after the first chunk.
- renderToNodeStream works the same in every build of the server renderer.
- Streaming makes the total server render time shorter.