skip to content

Server-Side Rendering

Rendering Vue on the server with the vue package alone: createSSRApp, string and stream renderers, hydration, SSR-safe code and lazy hydration. Interviewers probe code that runs twice.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

19

In a Vue 3 SSR app, what is the difference between createSSRApp and createApp, and which one must the browser entry call?

level: juniorimportance: must knowfreq 58%

answer

  1. same arguments, different mount
  2. hydrate versus replace
  3. container content on mount
  4. shared factory uses one

basics

~20 s

createSSRApp creates an app whose mount() hydrates the server-rendered DOM in the container; createApp's mount() clears the container and renders from scratch. The browser entry of an SSR app must use createSSRApp, usually through the shared app factory.

solid answer

~40 s

Both are exported from `vue` and take the same arguments; the docs say `createSSRApp` usage is exactly the same as `createApp`. The difference is what `mount()` does in the browser. An app from `createApp` clears the container (`textContent = ''`) and mounts fresh DOM, so server HTML is thrown away and rebuilt. An app from `createSSRApp` uses Vue's hydration renderer: `mount()` walks the existing DOM, attaches event listeners and adopts the nodes, repairing any mismatches. If the container is empty it warns and performs a full mount instead. On the server, `renderToString` accepts either, but the universal factory calls `createSSRApp` so the same code hydrates on the client.

code

ts · 7 lines
ts
// client.ts: browser entry of a Vue SSR app
import { createSSRApp } from 'vue'
import App from './App.vue'

// createApp(App).mount('#app') would wipe the server HTML and rebuild it.
// createSSRApp hydrates: it adopts the existing nodes and attaches listeners.
createSSRApp(App).mount('#app')

go deeper

for a junior

Remember the pairing: createSSRApp in the shared factory, mount() in the browser hydrates; createApp's mount() replaces whatever is in the container.

for a middle

Explain what hydration does on mount, adopting nodes and attaching listeners, and what the empty-container warning means when you see it.

for a senior

Recognise the silent failure of calling createApp on the client, measure it as needless DOM work and flicker, and enforce a single universal factory.

for a principal

Treat one universal app factory as the contract between server and client entries, so both sides cannot drift in plugins, state or root component.

## Two ways to create a Vue app Vue 3 exports two application factories from the `vue` package: - `createApp(rootComponent, rootProps?)`: the normal client-side app. - `createSSRApp(rootComponent, rootProps?)`: an app in **SSR hydration mode**. The API reference is explicit that `createSSRApp` usage is exactly the same as `createApp()`: same arguments, same `app.use`, `app.provide`, `app.component` and `app.config`. The difference only appears when you call `app.mount(container)` in the browser. ## What mount() does in each case | | `createApp(...).mount('#app')` | `createSSRApp(...).mount('#app')` | |---|---|---| | Existing content in the container | removed (`container.textContent = ''`) | kept and adopted | | DOM creation | builds every node from scratch | reuses server-rendered nodes | | Event listeners | attached to the new nodes | attached to the existing nodes | | Mismatch between server HTML and client render | not applicable | existing nodes are morphed to match | | Empty container | normal mount | warning, then a full mount | With `createSSRApp`, Vue lazily switches to its **hydration renderer**. Mounting then means walking the existing DOM alongside the component tree, matching each vnode to the node already there, attaching listeners, and running client-side hooks such as `onMounted`. That process is **hydration**. If the container has no children, the hydration renderer logs `Attempting to hydrate existing markup but container is empty. Performing full mount instead.` and mounts normally. Seeing that warning usually means the server HTML never reached the container: wrong selector, a shell that forgot to insert the rendered string, or a client-only route. ## Why using createApp in the browser is a bug Nothing throws, which is what makes it easy to miss: 1. The server renders full HTML and the browser paints it. 2. The client entry calls `createApp(App).mount('#app')`. 3. Vue empties the container and rebuilds the same DOM from scratch. The result is wasted work on the client, possible visual flicker, lost focus or scroll inside the replaced nodes, and none of the benefits of reusing the server's DOM. ## Which side calls which The usual structure is one **universal** factory, imported by both entries: ```ts import { createSSRApp } from 'vue' import App from './App.vue' export function createApp() { const app = createSSRApp(App) // register plugins, provide per-request state here return { app } } ``` - The **server entry** calls it per request and passes the app to `renderToString` or a stream renderer from `vue/server-renderer`. Those functions accept any app instance, but using the same factory keeps both sides identical. - The **client entry** calls it once and runs `app.mount('#app')`, which hydrates because the app came from `createSSRApp`. ## What does not change Choosing `createSSRApp` changes how the first mount treats existing DOM, and nothing else: - Components, plugins, `provide`/`inject` and `app.config` behave exactly as in a client-only app. - After hydration, updates are ordinary client-side patches; the app does not stay in a special mode. - `onMounted` still runs in the browser once the component's DOM, here the adopted server DOM, is in place. - Components still have to be SSR-safe: `createSSRApp` does not make browser-only code safe to run on the server. Internally the hydration-capable renderer is created lazily, the first time `createSSRApp` is called, so a purely client-side app never sets it up. ## Rules that still apply - `mount()` can only be called once per app instance, on either kind of app. - The selector passed to `mount()` must match the element the server wrapped the rendered HTML in. - The server and client must render the same component tree with the same state, or hydration has to repair the difference. - In development both factories add the same checks on the app config; the SSR variant changes only how mounting treats existing DOM.

  • In the browser, a Vue app created with createSSRApp logs 'Attempting to hydrate existing markup but container is empty.' What does that tell you?
    Hydration found no child nodes in the mount container, so Vue fell back to a full client mount. The server HTML did not reach that element: the shell may not insert the rendered string, the selector may differ from the wrapping element's id, or the page was served without SSR. The page still works, but you are paying for SSR without using it.
  • Can the Vue server entry pass an app made with createApp to renderToString?
    Yes. `renderToString` renders any app instance to a string; hydration mode only matters when `mount()` runs in the browser. Teams still build both sides from one factory that calls `createSSRApp`, because the client must hydrate and keeping a single code path avoids the two sides drifting apart.

saying these in an interview costs you the question

  • createApp detects server HTML in the container and hydrates it automatically.
  • createSSRApp is imported from vue/server-renderer, not from vue.
  • createSSRApp renders to a string on its own, without renderToString.
  • Mounting an SSR app into an empty container throws an error.
  • createSSRApp must never be called in the browser.
open as a page

A Vue 3 `<script setup>` component reads localStorage at the top of setup and the server render crashes. Why, and how do you fix it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Setup code, including the whole root scope of script setup, runs on the server, where there is no visitor's localStorage. Start from a server-safe default and read localStorage inside onMounted, which Vue never calls during SSR.

open as a page

A Vue 3 SSR component renders `new Date().toLocaleTimeString()` and hydration warns about a text mismatch; why, and how would you fix it?

level: middleimportance: must knowfreq 55%

basics

~20 s

The component renders once on the server and again in the browser, at a different instant and often in a different time zone and locale, so the text differs. Render a stable placeholder and set the time in onMounted, or make the value deterministic.

open as a page

When hand-rolling Vue 3 SSR on a small Node server, how do you split the code into universal, server and client entries?

level: middleimportance: must knowfreq 50%

basics

~20 s

A universal module exports a factory that calls createSSRApp. The server entry calls it per request, renders with renderToString and wraps the HTML in a page shell with state and the client script. The client entry calls it once, restores state and mounts.

open as a page

In a Vue 3 SSR app, a store created with reactive() at module scope shows one visitor's data to another visitor. Why, and what is Vue's recommended fix?

level: seniorimportance: must knowfreq 55%

basics

~20 s

On the server a module is evaluated once per process, so a module-level reactive() object is shared by every request. Vue's fix: create a new app per request, create the store inside that factory, and share it with app.provide.

open as a page

In a Vue 3 SSR app, why does a template containing `<p><div>Hi</div></p>` cause a hydration mismatch even though the server rendered exactly that?

level: juniorimportance: should knowfreq 42%

basics

~20 s

The browser's HTML parser, not Vue, turns the server string into DOM, and it closes the paragraph before the div. The resulting DOM no longer matches the vnode tree the client renders, so Vue reports a mismatch and rebuilds those nodes.

open as a page

In a Vue 3.5 SSR app, what is lazy hydration, and how do you enable it for one component?

level: juniorimportance: should knowfreq 30%

basics

~10 s

Lazy hydration lets a server-rendered async component stay as inert server HTML until a strategy tells Vue to hydrate it. Enable it with defineAsyncComponent's hydrate option, for example hydrate: hydrateOnVisible().

open as a page

In Vue 3.5, what does the data-allow-mismatch attribute do, which values does it accept, and when is it the wrong fix?

level: middleimportance: should knowfreq 35%

basics

~20 s

data-allow-mismatch (Vue 3.5+) marks a hydration mismatch as expected, so Vue stops reporting it; it does not change how Vue recovers. Values are text, children, class, style or attribute, comma-separated, or empty for all. It is wrong for bugs you can fix.

open as a page

In a Vue 3.5 SSR app, why should a form component generate its label and input ids with useId() instead of Math.random() or a counter?

level: middleimportance: should knowfreq 40%

basics

~20 s

Random values and module counters produce different ids on the server and in the browser, so id and for attributes mismatch during hydration. useId, added in Vue 3.5, derives the id from the component's place in the app tree, so both renders agree.

open as a page

In Vue 3.5, what does each built-in hydration strategy wait for, and how do you choose between them?

level: middleimportance: should knowfreq 25%

basics

~20 s

hydrateOnIdle waits for idle time (10 s cap by default), hydrateOnVisible for a root entering the viewport, hydrateOnMediaQuery for a matching query, hydrateOnInteraction for a listed event, which it replays. Choose by when the user first needs it.

open as a page

In Vue 3's vue/server-renderer, how do renderToString and the stream renderers differ, and how do you choose between them?

level: middleimportance: should knowfreq 38%

basics

~20 s

renderToString 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.

open as a page

In Vue 3 SSR, what does onServerPrefetch do, and why must the data it fetches also reach the client?

level: middleimportance: should knowfreq 45%

basics

~20 s

onServerPrefetch registers an async callback that Vue's server renderer awaits before rendering that component, so fetched data lands in the HTML. The client never calls it, so the data must be serialized and restored, or the client starts empty and refetches.

open as a page

During Vue 3 server rendering, what happens to reactivity, and how do watch, watchEffect and computed behave inside a component's setup?

level: middleimportance: should knowfreq 38%

basics

~20 s

Vue renders each component once on the server with no render effect, so state changes never re-render anything. In setup, lazy and post-flush watchers never run, watchEffect and immediate watchers run once then stop, and computed evaluates when read.

open as a page

A Vue 3.5 SSR app logs only 'Hydration completed but contains mismatches.' in production, and a theme class is stuck wrong after load; how do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Production builds print one generic error, so reproduce with a development SSR build or a production build with VUE_PROD_HYDRATION_MISMATCH_DETAILS enabled. The stuck class is likely a separate, silent mismatch: class differences are not corrected, and plain production builds do not even check them.

open as a page

A Vue 3.5 SSR article page has a below-the-fold comments widget; what do you gain and give up by hydrating it with hydrateOnVisible()?

level: seniorimportance: should knowfreq 25%

basics

~20 s

You move the widget's setup, render and listener work off the initial hydration, so the article becomes interactive sooner. You do not save its download, it stays inert until visible, and updating its props early abandons the deferral.

open as a page

When rendering a Vue 3 app on the server, what is the SSR context object, and how do useSSRContext and ctx.teleports rely on it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The SSR context is the object passed as the second argument to renderToString or a stream renderer. Components read it with useSSRContext() to record data for the handler, and Vue fills ctx.teleports with teleported HTML keyed by target.

open as a page

Would you hand-roll SSR on Vue core's createSSRApp and vue/server-renderer, or adopt a Vue meta-framework, and what decides it?

level: principalimportance: should knowfreq 28%

basics

~20 s

Vue core provides rendering and hydration only; builds, asset links, routing, data loading, state transfer, head tags and error pages are yours. Vue's docs recommend a framework for most SSR apps; hand-rolling suits small, contained cases needing control.

open as a page

How do you write a custom Vue 3.5 HydrationStrategy that hydrates when the browser is idle or on the first click, whichever comes first?

level: seniorimportance: nice to knowfreq 15%

basics

~10 s

A HydrationStrategy receives hydrate and forEachElement and may return a teardown. Compose hydrateOnIdle() and hydrateOnInteraction('click') by calling each with a guarded hydrate-once callback, and return a teardown that stops both.

open as a page

Why does a Vue 3 custom directive that sets an attribute in its mounted hook leave no trace in server-rendered HTML, and what does getSSRProps do?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Custom directive hooks such as mounted work on a live DOM element, and the server renders strings, so Vue ignores them during SSR. An object directive's getSSRProps(binding) returns props that the server renderer merges into that element's rendered attributes.

open as a page