Skip to the solution
HardFrontend12 min read

How does server-side rendering work in Vue (renderToString, hydration)?

175 views

The problem

Question presented to candidate: "Our Vue storefront is a client-rendered SPA and the marketing team says search engines and slow phones see a blank page first. Walk me through how server-side rendering works in Vue: what happens on the server, what gets sent, and what the browser does with it."

What a strong answer should cover:

  • The server runs the same components and turns them into an HTML string with renderToString (or a stream) from vue/server-renderer; templates are compiled into string-concatenating ssrRender functions.
  • Only setup(), onServerPrefetch and render run on the server. onMounted and other DOM hooks do not, and there is no reactivity-driven re-render.
  • The browser receives real markup (fast first paint, crawlable), plus the JS bundle and serialised state.
  • Hydration: createSSRApp(...).mount() walks the existing DOM, attaches event listeners and adopts the nodes instead of re-creating them; createApp would throw the markup away.
  • The app factory pattern (fresh app and store per request) to avoid cross-request state pollution.
  • Costs: server CPU, a Node runtime to host, code that must not touch window during setup, and hydration mismatches.

Clarifying questions expected:

  • "Is the content per-user, or could some pages be prerendered at build time instead?"
  • "Do we want to own the server setup, or use a framework such as Nuxt?"

Code / implementation expected: Optional β€” a minimal server that calls renderToString plus a client entry that hydrates with createSSRApp.

  • vue
  • ssr
  • hydration

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 12 sections

Read the code

Minimal SSR round trip: app factory, renderToString server, hydrating client
// ── app.js ──────────────────────────────────────────────
// app.js: shared by server and client. A FACTORY, so every request gets fresh state.
import { createSSRApp, h, ref } from "vue";

export function createApp(initialState) {
  const todos = ref(initialState.todos);
  const app = createSSRApp({
    setup() {
      const add = () => todos.value.push(`Task ${todos.value.length + 1}`);
      return () => h("main", [
        h("h1", "Todos"),
        h("ul", todos.value.map((t) => h("li", { key: t }, t))),
        h("button", { onClick: add }, `Add (${todos.value.length})`),
      ]);
    },
  });
  return { app, state: { todos } };
}

// ── server.js ───────────────────────────────────────────
// server.js: render to a string, embed the state, send one HTML document.
import http from "node:http";
import { renderToString } from "vue/server-renderer";
import { createApp } from "./app.js";

http.createServer(async (req, res) => {
  if (req.url !== "/") return res.writeHead(404).end();
  const initialState = { todos: ["Write docs", "Ship 3.5"] }; // e.g. from a database
  const { app } = createApp(initialState);
  const html = await renderToString(app);
  // JSON inside <script>: escape "<" so a value like "</script>" cannot end the tag early.
  const state = JSON.stringify(initialState).replace(/</g, "\\u003c");
  res.setHeader("content-type", "text/html");
  res.end(`<!doctype html><html><body>
<div id="app">${html}</div>
<script>window.__STATE__ = ${state}</script>
<script type="module" src="/client.js"></script>
</body></html>`);
}).listen(5265);

// ── client.js (bundled for the browser by Vite or esbuild) ──
// client.js: same app, same state, then hydrate the existing DOM instead of re-creating it.
import { createApp } from "./app.js";
const { app } = createApp(window.__STATE__);
app.mount("#app"); // createSSRApp + mount over server markup = hydration

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 91 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextHard

How do you diagnose and fix hydration mismatches in Vue SSR / Nuxt 3?

Continue