---
title: "Dynamic Rendering in Vue: Why to Avoid It"
description: "Google calls dynamic rendering a workaround, not a fix. See why, and how to migrate a Vue SPA to SSR instead."
canonical_url: "https://nuxtseo.com/learn-seo/vue/spa/dynamic-rendering"
last_updated: "2026-07-16"
---

<key-takeaways>

- Google calls dynamic rendering a workaround, not a long-term solution, and recommends server-side rendering or static generation instead
- It isn't cloaking as long as bots and users see similar content; cloaking only applies when the two versions diverge completely
- Running two rendering paths doubles your infrastructure and does nothing for the Core Web Vitals real users experience
- Nuxt renders on the server by default, so a Vue SPA can drop dynamic rendering by adopting it

</key-takeaways>

Dynamic rendering detects crawler requests and serves them pre-rendered HTML while real users get the client-rendered app. It was a common fix for SPA indexing problems before SSR tooling matured. Google's own guidance now calls it a workaround, not a long-term solution, and Vue's SSR options make it unnecessary for a new project.

<warning>

Google treats dynamic rendering as a workaround, not a recommended solution, and points to server-side rendering or static rendering instead. It doesn't count as cloaking if bot and human content stay similar, but it doubles your infrastructure and fixes nothing for the users hitting your slow client-rendered app.

</warning>

## Why Google Recommends Against It

Google [positions dynamic rendering](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) as added complexity and maintenance burden: you're running two rendering paths for one site instead of one. The practical problems:

- You maintain two versions of the app, one for bots and one for humans, and every feature needs testing against both
- User-agent sniffing is unreliable; new bots and crawler updates break your detection logic over time
- Real users still get the slow client-rendered app; dynamic rendering does nothing for INP or the other Core Web Vitals they experience
- Keeping the two versions in sync well enough that they never diverge is real ongoing work, and defeats the point of avoiding SSR in the first place

## Is It Cloaking?

Not inherently. Google says it "generally doesn't consider dynamic rendering as cloaking" as long as both versions serve similar content. Cloaking is serving different content to users and crawlers: Google's own example is a page about cats for users and a page about dogs for crawlers. The risk with dynamic rendering isn't a manual penalty for using it; it's the maintenance cost of keeping two code paths close enough that they never drift into that territory.

## Migrating to SSR

If you're running dynamic rendering or a legacy render service, here's the path off it:

1. **Audit dependencies.** Find Vue libraries that touch `window` or `document` on import; they break under SSR.
2. **Adopt a framework.** [Nuxt](https://nuxt.com) handles SSR, hydration, and data fetching by default. `vite-ssg` works if your site is fully static. Custom SSR with [Vite](https://vite.dev) is possible but only worth it for teams that need the control.
3. **Wrap client-only code.** Use `<ClientOnly>` components or `onMounted()` hooks for anything that strictly requires the browser.
4. **Roll out incrementally.** Route a percentage of traffic to the new SSR app at your edge router while the rest stays on the old setup.
5. **Remove the dynamic renderer.** Once SSR is stable, delete the bot-detection middleware and serve everyone the same HTML.

## How Dynamic Rendering Worked

Legacy implementations detected crawler user agents and proxied the request to a headless rendering service:

```ts [Express (Legacy)]
import express from 'express'
import rendertron from 'rendertron-middleware'

const app = express()

// User-agent sniffing decided which version to serve
app.use(rendertron.makeMiddleware({
  proxyUrl: 'https://render-tron.appspot.com/render',
  userAgentPattern: /googlebot|bingbot|slurp|duckduckbot/i
}))
```

This needed a headless browser, Puppeteer or Rendertron, to load the page, wait for network idle, and serialize the resulting HTML for every bot request. That round trip was slow, added infrastructure to operate, and broke whenever the headless renderer fell behind the live site.

Nuxt renders on the server by default, so there's no dynamic rendering workaround to build or maintain. Learn more about [rendering modes in Nuxt](/learn-seo/nuxt/routes-and-rendering/rendering).

## Checklist

<checklist id="vue-dynamic-rendering">

- Dynamic rendering, if present, has a migration plan toward SSR or SSG
- Identify client-only dependencies and wrap them in `<ClientOnly>` or `onMounted()`
- Bot and human responses are verified to serve equivalent content while both paths still run
- Remove bot-detection middleware once SSR ships

</checklist>
