DEV Community

Cover image for Nuxt 3 Rendering for B2B SaaS: SSR, SPA or Hybrid?
Muhammad Ali Mughal
Muhammad Ali Mughal

Posted on

Nuxt 3 Rendering for B2B SaaS: SSR, SPA or Hybrid?

Most B2B SaaS teams pick a Nuxt rendering mode once, at project setup, and then live with the consequences for years. Either everything is server-rendered, so the logged-in dashboard pays for a Node server it does not need, or everything is a single-page app, so the pricing and docs pages that should rank in search ship an empty <div id="__nuxt">. Nuxt 3 does not force that choice. With route rules you can decide per route, and for a typical B2B product the right answer is almost always hybrid.

At Loop Origin we build customer portals, dealer ordering systems and SaaS dashboards on Nuxt 3 with Laravel or Node backends. This is the decision framework I use, the exact nuxt.config.ts we start from, and the failure modes that have cost real debugging time.

Should a B2B SaaS app use SSR, SPA or hybrid rendering in Nuxt 3?

Direct answer: Use hybrid rendering. Prerender or cache public marketing, pricing and docs pages for SEO and fast first paint, and serve the authenticated app as a client-rendered SPA with ssr: false route rules. Reserve full server rendering for public pages whose content changes per request, and never cache any server-rendered HTML that depends on the user's session cookie.

The four rendering modes, compared

Mode How HTML is produced SEO First load (TTFB) Server needed Best fit in B2B SaaS
SSR (universal) Rendered by Nitro on every request Strong Depends on API latency per request Yes, Node or edge runtime Public pages with per-request data
SPA (ssr: false) Empty shell, rendered in the browser Weak for content pages Fast shell, slower meaningful paint No, static hosting is enough Logged-in dashboards, admin panels
Prerender (SSG) Rendered once at build time Strong Very fast, served from CDN No Marketing, pricing, docs, changelog
SWR / ISR cache Rendered on demand, cached, revalidated Strong Fast after the first hit Yes, or a platform with ISR support Large catalogs, public listings, help centers

The column people skip is "Server needed". A pure SPA plus prerendered marketing pages can be served from any static host or CDN. One runtime SSR route means running a Node process or edge function.

Map routes to modes, not the whole app

For a typical B2B product, I split routes into four groups:

  1. Marketing and pricing (/, /pricing, /features/**): content changes on deploy. Prerender.
  2. Docs, blog, changelog (/docs/**, /blog/**): many pages, edited through a CMS. Prerender, or SWR if you publish without redeploying.
  3. Public dynamic pages (/integrations/**, public status or listing pages): data from an API, same for every visitor. SWR with a sensible revalidation window.
  4. Authenticated app (/app/**, /account/**, /admin/**): per-user data, never indexed. SPA.

Here is the configuration we start from:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/pricing': { prerender: true },
    '/features/**': { prerender: true },
    '/docs/**': { swr: 3600 },              // cache 1h, revalidate in background
    '/integrations/**': { swr: 600 },
    '/app/**': { ssr: false },                // client-rendered dashboard
    '/account/**': { ssr: false },
    '/admin/**': { ssr: false, headers: { 'X-Robots-Tag': 'noindex' } },
    '/api/**': { headers: { 'Cache-Control': 'no-store' } },
    '/old-pricing': { redirect: { to: '/pricing', statusCode: 301 } },
  },
  nitro: {
    prerender: { crawlLinks: true, routes: ['/sitemap.xml'] },
  },
})
Enter fullscreen mode Exit fullscreen mode

Two production notes. First, swr caching is handled by Nitro's cache layer, which defaults to in-memory storage; on a multi-instance deployment each instance keeps its own cache unless you configure a shared storage driver such as Redis. Second, isr route rules are only meaningful on platforms that implement them (Vercel and Netlify, for example). On a plain VPS, swr is the equivalent you actually get.

A winding road through a green valley with a branching side road, representing per-route rendering decisions

SEO and performance: what actually changes

Search engines can render JavaScript, but in my experience pages that ship complete HTML index more reliably and preview correctly on LinkedIn, Slack or X, whose link-preview crawlers generally do not run JavaScript.

For TTFB, the trade-offs are structural, not magic:

  • Prerendered pages are static files; TTFB is whatever your CDN delivers.
  • SSR TTFB includes every useFetch or useAsyncData call that runs on the server before HTML is sent. One slow backend endpoint makes every page slow. Parallelize and time-box fetches.
  • SPA routes return a tiny shell fast, then the browser downloads JavaScript and calls your API. Perceived speed depends on bundle size and API latency, acceptable behind a login.

Measure your own routes:

curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://app.example.test/pricing
curl -s -o /dev/null -w 'ttfb=%{time_starttransfer}s total=%{time_total}s\n' https://app.example.test/docs/getting-started
Enter fullscreen mode Exit fullscreen mode

Run each twice; with swr, the second request should come from cache.

Hosting cost drivers by mode

Exact prices change with provider and traffic, but the cost structure is predictable:

Setup What you pay for Cost behaviour as traffic grows Ops burden
SPA + prerendered pages on a static host/CDN Bandwidth, build minutes Grows slowly; static files are cheap to serve Lowest
Hybrid on one VPS (Nitro node server behind Nginx) Fixed monthly server Flat until CPU is saturated by SSR Medium: process manager, updates, monitoring
Hybrid on serverless or edge Invocations and compute time Scales with SSR requests; cached routes reduce it Low to medium
Full SSR on every route Compute for every page view, including the dashboard Grows fastest; you pay to render pages nobody indexes Medium to high

The expensive mistake is full SSR for the authenticated app: you pay server compute to render dashboards for logged-in users, gain nothing in SEO, and add a second place where auth can go wrong. My VPS vs managed PaaS TCO breakdown covers the hosting decision itself, and multi-stage Dockerfiles for PHP and Node shows how we package the Nitro server.

Failure modes I have debugged in production

1. Auth cookies leaking into cached SSR

This is the dangerous one. A page under an swr rule reads the session cookie during SSR (for example, a header that shows "Signed in as ..."), and Nitro caches the rendered HTML keyed by URL. The next anonymous visitor receives the first user's name, or worse, their data.

Rules we enforce:

  • Any route that reads cookies, useRequestHeaders(['cookie']) or session state on the server must not have swr, isr or prerender.
  • Personalized fragments on public pages render client-side only, inside <ClientOnly>.
  • Cached responses must never carry Set-Cookie. Check with curl -sI against a cached route.

2. Hydration mismatches

Vue warns "Hydration completed but contains mismatches" when server HTML differs from the first client render. The usual causes in B2B apps are dates and time zones (server in UTC, browser in the user's zone), Math.random() or Date.now() in templates, and reading localStorage or window during setup. Fixes: format dates on the client or pass the user's time zone explicitly, generate IDs with useId(), and move browser-only logic into onMounted or <ClientOnly>.

3. Forwarding cookies to the API during SSR

On the server, $fetch to your Laravel or Node API does not automatically carry the browser's cookies. If an SSR page needs authenticated data, use useRequestFetch() or pass headers: useRequestHeaders(['cookie']) explicitly, and keep that route out of every cache rule.

4. Middleware that assumes the browser

Route middleware runs on the server for SSR routes and on the client for SPA navigation. Code that reads localStorage for a token breaks on the server. With ssr: false on /app/**, that middleware runs only in the browser.

Rendering decision checklist

  1. List every route group and mark it public or authenticated.
  2. Authenticated groups: ssr: false, plus noindex for admin areas.
  3. Public pages that change only on deploy: prerender: true.
  4. Public pages with API data shared by all visitors: swr with a revalidation window you can defend.
  5. Confirm no cached route reads cookies or session state on the server.
  6. Configure shared Nitro cache storage if you run more than one instance.
  7. Measure TTFB per route group with curl -w before and after.
  8. Check cached responses for stray Set-Cookie headers.
  9. Test the hydration console warnings on the top five public pages.
  10. Document the rules in the repo so new routes get a deliberate mode.

If you are planning a portal or SaaS front end, this split is the default in our custom web application development work; the pricing page shows how we scope it.

Frequently asked questions

Is SSR required for SEO in Nuxt 3?

Not for every route. Public content pages should ship full HTML, which prerendering or SWR caching provides without per-request rendering. Authenticated dashboards are not indexed, so client-side rendering is fine there and avoids server cost.

What is the difference between swr and isr route rules?

Both serve cached HTML and revalidate it. swr is implemented by Nitro's own cache and works on a Node server or VPS. isr relies on the hosting platform's incremental static regeneration support, such as Vercel or Netlify, and has no effect where the platform does not implement it.

Can I mix SSR and SPA routes in one Nuxt 3 app?

Yes. Route rules let you set ssr: false on specific path patterns while the rest of the app stays universal or prerendered. It is still one build.

Why does my Nuxt page show another user's data?

Most often because a server-rendered page that reads the session cookie is under a cache rule such as swr, isr or a CDN cache. The first rendered HTML is cached and served to everyone. Remove caching from that route or render personalized parts on the client only.

Do I need a Node server to deploy Nuxt 3?

Only if some routes render at request time. An app made entirely of prerendered pages and ssr: false routes can be generated as static files and served from a CDN. Any SSR or SWR route requires the Nitro server, a serverless function or an edge runtime.

How do I fix hydration mismatch warnings?

Make the first client render match the server HTML. Avoid random values and current time in templates, format dates consistently or on the client, use useId() for generated IDs, and move access to window or localStorage into onMounted or a <ClientOnly> wrapper.


Follow & Connect

Top comments (0)