Dynamic Rendering SEO Guide: Serve JavaScript Sites to Googlebot
Moderate 14 min 2026-03-20

Dynamic Rendering SEO Guide: Serve JavaScript Sites to Googlebot

Quick Summary

  • What this covers: Implement dynamic rendering to serve pre-rendered HTML to Googlebot while keeping JavaScript for users. Fix indexing issues on React, Vue, Angular sites.
  • Who it's for: site owners and SEO practitioners
  • Key takeaway: Read the first section for the core framework, then use the specific tactics that match your situation.

Dynamic rendering serves pre-rendered static HTML to Googlebot while delivering JavaScript-heavy single-page applications (SPAs) to users, solving indexing failures on React, Vue, and Angular sites where content renders client-side after initial page load. Google's JavaScript crawler executes scripts but adds latency (2-5 seconds per page), exhausts crawl budget on render-intensive pages, and sometimes fails to execute complex frameworks that rely on newer JavaScript features. Dynamic rendering eliminates JavaScript execution delays for bots by detecting user-agent strings and serving fully-rendered HTML from headless Chrome snapshot or pre-built static exports, while maintaining interactive SPA experience for human visitors. Incorrect implementations — serving different content to users vs. bots (cloaking violations), failing to update pre-rendered snapshots when content changes, or blocking dynamic rendering URLs in robots.txt — trigger manual actions or leave bots indexing stale content. This guide implements dynamic rendering with Rendertron, Prerender.io, and Next.js/Nuxt.js frameworks without violating Google's cloaking policies.

When Dynamic Rendering Is Necessary

Google recommends server-side rendering (SSR) or static site generation (SSG) over dynamic rendering. Use dynamic rendering only when SSR/SSG aren't feasible.

Use Dynamic Rendering When:

1. Legacy SPA with poor SSR support Older React/Vue/Angular apps built without SSR. Refactoring to SSR requires rewriting architecture.

2. Complex client-side dependencies Third-party widgets, maps, real-time data fetches that don't server-render.

3. Resource constraints SSR requires Node.js server infrastructure. Dynamic rendering works with static hosting + proxy.

4. JavaScript-heavy sites with indexing issues Search Console shows "Discovered - currently not indexed" for pages with client-side content.

Don't Use Dynamic Rendering When:

1. SSR is possible Modern frameworks (Next.js, Nuxt.js, SvelteKit) handle SSR natively. Use SSR instead.

2. Content is static Static site generators (Gatsby, Hugo, Astro) pre-build HTML at deploy time. No dynamic rendering needed.

3. Google already indexes content correctly Test with URL Inspection Tool. If content appears, JavaScript rendering works — don't add complexity.

Phase 1: Diagnose JavaScript Indexing Issues

Verify Google indexes JavaScript-rendered content before implementing dynamic rendering.

Test with Google Search Console URL Inspection

  1. Search Console → URL Inspection
  2. Enter page URL
  3. Click "Test live URL"
  4. View "Crawled page" → "Screenshot"

Compare:

Problem indicators:

Check Indexed Content

site:example.com "unique text from JavaScript-rendered content"

If no results: Google isn't indexing JavaScript-rendered content.

Validate with JavaScript Disabled

Chrome DevTools:

  1. Settings → Debugger → Disable JavaScript
  2. Reload page
  3. Check if content appears

If page is blank with JS disabled: Googlebot may struggle to index content, even though Google executes JavaScript.

Phase 2: Choose Dynamic Rendering Solution

Three approaches with different complexity and cost.

Option 1: Rendertron (Google's Open-Source Solution)

Self-hosted headless Chrome renderer.

Pros:

Cons:

Best for: Developers comfortable managing infrastructure.

Option 2: Prerender.io (SaaS Solution)

Commercial service, $25-$200+/month.

Pros:

Cons:

Best for: Teams wanting turnkey solution.

Option 3: Framework-Native SSR (Next.js, Nuxt.js)

Not technically "dynamic rendering" but achieves same goal.

Pros:

Cons:

Best for: New projects or sites willing to refactor.

Phase 3: Implement Rendertron (Self-Hosted)

Rendertron runs headless Chrome, renders pages, returns HTML.

Install Rendertron

Docker (easiest):

docker pull rendertron/rendertron
docker run -p 3000:3000 rendertron/rendertron

Or Node.js:

git clone https://github.com/GoogleChrome/rendertron.git
cd rendertron
npm install
npm run build
npm start

Rendertron runs on http://localhost:3000

Configure Web Server to Proxy Bot Requests

Nginx configuration:

server {
  server_name example.com;

  location / {
    # Detect bot user-agents
    if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider") {
      proxy_pass http://localhost:3000/render/https://example.com$request_uri;
      break;
    }

    # Serve normal SPA for users
    try_files $uri $uri/ /index.html;
  }
}

Apache (.htaccess):

RewriteEngine On

# Detect bots
RewriteCond %{HTTP_USER_AGENT} googlebot|bingbot|yandex|baiduspider [NC]
RewriteRule ^(.*)$ http://localhost:3000/render/https://example.com/$1 [P,L]

Test Rendertron

Render a page:

curl http://localhost:3000/render/https://example.com

Should return fully-rendered HTML with JavaScript-generated content.

Cache Rendered Pages

Rendertron doesn't cache by default. Add caching layer:

Nginx caching:

proxy_cache_path /var/cache/nginx/rendertron levels=1:2 keys_zone=rendertron_cache:10m max_size=1g inactive=60m;

location / {
  if ($http_user_agent ~* "googlebot|bingbot") {
    proxy_cache rendertron_cache;
    proxy_cache_valid 200 24h;
    proxy_pass http://localhost:3000/render/https://example.com$request_uri;
    break;
  }
}

Phase 4: Implement Prerender.io (SaaS)

Commercial alternative to self-hosting.

Sign Up and Configure

  1. Create account at prerender.io
  2. Get API token
  3. Add domain

Configure Web Server

Nginx middleware:

# Install prerender middleware
git clone https://github.com/prerender/prerender-nginx.git

nginx.conf:

server {
  location / {
    set $prerender 0;

    if ($http_user_agent ~* "googlebot|bingbot|yandex|baiduspider") {
      set $prerender 1;
    }

    if ($prerender = 1) {
      proxy_pass https://service.prerender.io;
      proxy_set_header X-Prerender-Token YOUR_TOKEN;
      proxy_set_header X-Prerender-Host $host;
      break;
    }

    try_files $uri $uri/ /index.html;
  }
}

Recache Pages When Content Updates

Webhook to Prerender.io after publishing new content:

curl -X POST https://api.prerender.io/recache \
  -H "X-Prerender-Token: YOUR_TOKEN" \
  -d '{"url": "https://example.com/updated-page"}'

Integrate with CMS: Trigger recache on save/publish.

Phase 5: Avoid Cloaking Violations

Cloaking = showing different content to bots vs. users. Google penalizes cloaking.

Ensure Content Equivalence

Rendered HTML for bots must match JavaScript-rendered content for users.

Allowed differences:

Not allowed:

Test Both Versions

User version:

curl https://example.com

Bot version (with User-Agent):

curl -A "Googlebot" https://example.com

Compare content: Key text, headings, links should match.

Use Google's Mobile-Friendly Test

https://search.google.com/test/mobile-friendly

Shows rendered page as Googlebot sees it. Compare to user-facing version.

Phase 6: Monitor Dynamic Rendering Performance

Pre-rendering adds latency for bots. Monitor to ensure fast responses.

Track Bot Response Times

Server logs show Rendertron response times:

grep "Googlebot" /var/log/nginx/access.log | awk '{print $NF}' | sort -n

Target: <2 seconds per page

If slower:

Monitor Cache Hit Rates

Nginx cache stats:

grep "HIT" /var/log/nginx/access.log | wc -l
grep "MISS" /var/log/nginx/access.log | wc -l

Target: >80% hit rate

Low hit rate = wasted crawl budget (every bot request re-renders).

Phase 7: Transition to SSR (Long-Term Solution)

Dynamic rendering is a workaround. SSR is superior.

Migrate React SPA to Next.js

Next.js adds SSR to React with minimal refactoring.

Install:

npx create-next-app@latest

Convert pages:

// pages/products/[id].js (Next.js)
export async function getServerSideProps(context) {
  const { id } = context.params;
  const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json());

  return { props: { product } };
}

export default function Product({ product }) {
  return <div>{product.name}</div>;
}

SSR benefits:

Migrate Vue SPA to Nuxt.js

Nuxt.js = Next.js for Vue.

Install:

npx nuxi@latest init my-app

SSR by default. Existing Vue components work with minimal changes.

Frequently Asked Questions

Does dynamic rendering violate Google's cloaking policy?

No, if content is equivalent. Google's official guidance allows dynamic rendering for JavaScript-heavy sites. Key requirement: rendered HTML shown to bots must match JavaScript-rendered content shown to users. Removing ads or simplifying styles for bots is acceptable. Changing product info, prices, or content is cloaking.

Is dynamic rendering slower than SSR?

Yes. Dynamic rendering requires headless Chrome to render each page, adding 1-3 seconds. SSR renders on server at request time (faster, 100-300ms). SSG (static site generation) pre-renders at build time (fastest, <10ms). Use dynamic rendering only when SSR/SSG aren't viable.

How do I update pre-rendered content when my database changes?

Invalidate cache after content updates. With Prerender.io, use recache API. With Rendertron + Nginx, purge cache:

rm -rf /var/cache/nginx/rendertron/*

Or programmatic purge:

curl -X PURGE https://example.com/updated-page

Automate with webhooks from CMS on publish/update events.

Can I use dynamic rendering for mobile-only or desktop-only?

Yes, but not recommended. Detect device type along with user-agent:

if ($http_user_agent ~* "mobile.*googlebot") {
  # Render mobile version
}
if ($http_user_agent ~* "googlebot" and $http_user_agent !~* "mobile") {
  # Render desktop version
}

Better approach: Responsive design (single HTML works on all devices). Dynamic rendering adds complexity. See Core Web Vitals mobile optimization.

Should I use dynamic rendering for my React/Next.js app?

No. If using Next.js, you already have SSR or SSG — no dynamic rendering needed. Dynamic rendering is for legacy SPAs without SSR capability. Next.js, Nuxt.js, SvelteKit handle server rendering natively. Only use dynamic rendering if refactoring to SSR framework isn't feasible.


When This Fix Isn't Your Priority

Skip this for now if:

This is one piece of the system.

Built by Victor Romo (@b2bvic) — I build AI memory systems for businesses.

← All Fixes