Setting Up Server-Side Rendering in Quasar: A Practical Task Guide
Convert a Quasar SPA to server-side rendering: add SSR mode, guard browser-only code, use preFetch for per-request data, deploy the Node bundle, and verify hydration.
31 May 2026, 23:51 UTC

The outcome you want
You have a Quasar single-page application and you need the first page load to arrive as rendered HTML instead of an empty <div id="q-app"> waiting on JavaScript. The usual reasons are SEO, faster perceived load on slow connections, or link previews. Quasar's SSR mode gives you this without rewriting the app: the same Vue components render on a Node.js server, then hydrate in the browser.
The trade-off to accept up front: SSR adds a Node.js runtime dependency to your deployment, and any code that touches browser-only APIs (window, document, localStorage) must be guarded or moved into client-only lifecycle hooks. Plan for that audit before you start.
Prerequisites
- A Quasar project created with the Quasar CLI (Vite-based or Webpack-based both support SSR; the config file location differs slightly).
- Node.js 14 or later on both your development machine and the production host. Older Node versions can fail during the SSR bundle step.
- Permission to run
quasarCLI commands in the project directory and to install dependencies. - A rough inventory of where your app uses browser-only globals. Search for
window.,document., andlocalStoragebefore converting.
Adding SSR mode to an existing project
Run this from the project root, with your normal development user (no elevated privileges needed):
quasar mode add ssrThis scaffolds the SSR entry files under src-ssr/ and registers the mode in your configuration. It does not change your existing SPA code. The key file is src-ssr/server.js (or the middlewares structure in newer scaffolds), which is the Node.js entry point that renders your app per request.
Next, confirm the mode is active in quasar.config.js (Vite-based projects) or quasar.conf.js (older Webpack-based projects). Look for the ssr section:
ssr: {
pwa: false,
prodPort: 3000,
middlewares: [
'render'
]
}Start the SSR dev server:
quasar dev -m ssrExpected check: the page loads as before, but view-source (not the DOM inspector — the actual source, e.g. Ctrl+U in most browsers) now shows rendered markup for your routes rather than an empty app container. If view-source still shows only the shell, the server is not rendering; check the terminal for errors and confirm you started with -m ssr.
Guarding browser-only code
This is where most SSR conversions break. Code that runs during setup or in created() executes on the server, where window does not exist. Quasar exposes a platform check for exactly this:
import { Platform } from 'quasar'
if (Platform.is.client) {
const saved = localStorage.getItem('token')
}Alternatively, move browser-only work into onMounted(), which never runs on the server. A common pattern that fails silently: reading a cookie or token in a boot file and storing it in state. On the server this runs per request, so per-user data can leak between requests if you store it in a module-level singleton. Keep request-specific state inside the app instance (e.g. via provide or your store's per-request initialization), not in imported module variables.
Data fetching per request
Quasar supports a preFetch hook on route components. It runs on the server before rendering (and on the client during navigation), so the HTML arrives with data already in the store:
export default {
async preFetch ({ store, currentRoute }) {
await store.dispatch('articles/load', currentRoute.params.id)
}
}Expected check: disable JavaScript in the browser and reload the route. The article content should still appear, because it was rendered server-side. If the page is empty without JS, your data is being fetched in onMounted instead of preFetch.
Production build and deployment
Build with:
quasar build -m ssrThe output lands in dist/ssr/ and includes its own package.json. On the production host, install dependencies inside that folder and start it with Node:
cd dist/ssr
npm install
node index.jsMeaningful placeholders to adjust: the listen port comes from ssr.prodPort in your config or the PORT environment variable; put a reverse proxy (nginx, Caddy) in front for TLS. Risk to note: the SSR server holds your Vue app in memory and renders per request, so memory usage scales with concurrency in a way a static SPA never does. Monitor it under load before announcing the migration.
Verifying hydration health
Hydration mismatches — where server HTML differs from the client's first render — are the most common SSR bug. Symptoms: content flickers or re-renders on load, and the browser console shows hydration warnings in development mode. Common causes:
- Rendering dates or random values during setup (they differ between server and client). Render them in
onMountedor suppress with a client-only wrapper. - Branching on
Platform.isin a way that changes the initial DOM tree. The server and client must produce the same initial markup. - Invalid HTML nesting that the browser's parser silently restructures, making the client DOM differ from the server string.
Practical check: load a page, open the console, and confirm there are no hydration warnings. Then compare view-source against the rendered DOM for one complex route. Test in at least two browsers, since hydration behavior and parser edge cases vary.
Recovery options
Because quasar mode add ssr only adds files, rolling back is safe: run quasar mode remove ssr, delete src-ssr/ if anything remains, and rebuild with quasar build -m spa. Your components are untouched except for any SSR guards you added, which are harmless in SPA mode. If production SSR misbehaves after deploy, revert the reverse proxy to point at your previous static SPA build while you diagnose — keep the last known-good dist/spa/ artifact until SSR has survived real traffic.
Limitations
SSR does not help routes behind authentication much, since crawlers never see them; consider whether only public routes justify the operational cost. Very large pages increase server render time linearly, so measure time-to-first-byte under realistic data. And if your app is heavily dependent on third-party libraries that assume a browser environment, each one needs individual verification — there is no general fix beyond lazy-loading them client-side.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.