Server-Side Rendering

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

By default a Vue app is client-rendered: the server sends an empty <div id="app"> and JavaScript builds the DOM in the browser. Server-side rendering (SSR) instead renders components to HTML strings on the server, sends usable markup, and then hydrates it in the browser. The guide covers this at Server-Side Rendering and the API at Server-Side Rendering API.

CSR, SSR, SSG, and ISR

Strategy What happens

CSR

HTML shell + JS; the browser renders. Simplest to deploy; slowest first paint; content invisible to crawlers that do not run JS.

SSR

HTML rendered per request on a Node/edge server, then hydrated. Fast first contentful paint and full SEO; needs a running server and careful state handling.

SSG

Pages rendered to static HTML at build time, hydrated on load. Best TTFB, host anywhere; only suits content known at build time.

ISR

SSG plus background regeneration: serve a cached static page, rebuild it on an interval or on demand. Static speed with fresher content; framework-specific.

The cost of SSR is complexity: code runs in two environments, so browser-only globals (window, document, localStorage) are off-limits during rendering, and build tooling gets more involved. Reach for a framework unless you have a specific reason not to.

createSSRApp, renderToString, and hydration

On the server, create the app with createSSRApp (not createApp) and render it with renderToString from vue/server-renderer:

// server entry
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'

export async function render() {
  const app = createSSRApp(App)
  const html = await renderToString(app)
  return `<!DOCTYPE html><div id="app">${html}</div><script type="module" src="/entry-client.js"></script>`
}

For large pages, the streaming renderers renderToNodeStream / renderToWebStream flush HTML as it is produced, improving time-to-first-byte.

On the client, build the same app with createSSRApp and call app.mount. Because the container already contains server-rendered markup, Vue hydrates instead of rendering from scratch: it walks the existing DOM and attaches reactivity and event listeners in place.

// client entry
import { createSSRApp } from 'vue'
import App from './App.vue'

createSSRApp(App).mount('#app')   // hydration, not a fresh render
sequenceDiagram participant Server participant Browser Server->>Server: createSSRApp(App) Server->>Server: renderToString(app) Server->>Browser: HTML markup + serialized state Browser->>Browser: parse HTML, show content (not yet interactive) Browser->>Browser: createSSRApp(App) + app.mount('#app') Browser->>Browser: hydrate: adopt existing DOM, attach listeners Note over Browser: page is now interactive

Hydration mismatches

Hydration assumes the client’s first render produces markup identical to the server’s. When it does not — a random value, a locale-dependent date, invalid HTML nesting that the browser "fixes" — Vue logs a hydration mismatch warning and patches that subtree client-side. Fix the nondeterminism, or, when a difference is intentional, mark the element with data-allow-mismatch to silence the warning for it. See Hydration Mismatch.

Data fetching and state transfer

Fetch on the server, serialize the result into the HTML, and reuse it on the client so hydration matches and no duplicate request fires:

  • onServerPrefetch(async () ⇒ \{ …​ }) — a lifecycle hook that runs only on the server; renderToString awaits it before producing HTML.

  • useSSRContext() — returns a per-request object you can write to on the server (for collected head tags, fetched data, status codes) and read back after renderToString resolves.

<script setup>
import { ref, onServerPrefetch } from 'vue'

const post = ref(null)

// on the server: fill before render. on the client: read from transferred state
onServerPrefetch(async () => {
  post.value = await fetchPost(route.params.id)
})
</script>

Cross-request state pollution

On the server the module scope is shared by every request. A store or piece of state created once at module load is therefore leaked across users. Create per-request state inside a factory instead:

// BAD on the server: one store shared by all requests
export const store = reactive({ user: null })

// GOOD: a fresh instance per request
export function createStore() {
  return reactive({ user: null })
}

The same rule is why Pinia takes state as a function and why the router uses createMemoryHistory per request.

Client-only content

Some components cannot render on the server — they need window, measure the DOM, or wrap a browser-only library. Wrap them so they render only after hydration:

  • <ClientOnly> (provided by Nuxt and VitePress; a few lines to write in a bare setup) renders its slot only on the client.

  • defineAsyncComponent plus a guard, or importing the library inside onMounted, achieves the same in plain Vue SSR.

Nuxt and VitePress

Wiring the server, client, and build entries by hand is involved. Two official-ecosystem tools do it for you:

  • Nuxt — the recommended full-stack framework for Vue. File-based routing, data fetching that transfers automatically, SSR / SSG / ISR / hybrid per route, an API layer (Nitro), and deployment presets for Node and edge platforms.

  • VitePress — a Vite-powered static-site generator aimed at documentation and content sites; Markdown authoring with Vue components available inline. It builds this kind of docs site.

See also