JavaScript-heavy web applications and SEO have been in conflict since single-page applications became mainstream. The core problem: Googlebot and other crawlers process JavaScript differently from browsers, and content that relies on client-side JavaScript execution is often not indexed reliably.
Dynamic rendering is one of three solutions to this problem. This guide explains when it’s the right choice, how to implement it, and what it looks like in practice for different stack types.
The JavaScript SEO Problem
When Googlebot crawls a page, it processes HTML immediately but queues JavaScript for a second wave of rendering. This second wave can happen anywhere from seconds to days after the initial crawl, and it uses a version of Chromium that may not support all modern JavaScript features.
The result for heavily JavaScript-dependent sites:
- Product listings, blog posts, or navigation menus that only exist in JavaScript may not be indexed
- Dynamic content loaded after the initial HTML (infinite scroll, client-side rendered categories) is often missed
- Internal links generated by JavaScript are not reliably discovered for crawling
- Page titles, meta descriptions, and structured data injected via JavaScript may not be processed
Three Solutions: Which One Fits Your Situation
Option 1: Server-Side Rendering (SSR) — Best Long-Term Solution
Generate HTML on the server for every request. The crawler receives complete HTML with all content. Best choice for new projects — use Next.js (React), Nuxt (Vue), or SvelteKit.
When to use: New applications, or when you have engineering bandwidth to refactor
Cost: Higher server compute, significant development investment for refactors
Option 2: Static Site Generation (SSG) — Best for Content-Heavy Sites
Pre-build all pages at deploy time as static HTML files. The crawler receives fully static HTML with zero JavaScript dependency for core content. Best for blogs, documentation, marketing sites with predictable content.
When to use: Content that doesn’t change per-user; sites where build-time generation is feasible
Cost: Build times increase with page count; not suitable for highly dynamic content
Option 3: Dynamic Rendering — Practical Workaround for Existing SPAs
Detect crawler user agents and serve pre-rendered HTML to them; serve the JavaScript application to real users. The crawler always receives complete, static HTML. Real users get the full JavaScript experience.
When to use: Existing SPAs where SSR refactoring is expensive; situations where quick SEO improvement is needed without full re-architecture
Cost: Additional infrastructure for the rendering layer; content may lag if pre-rendered HTML isn’t kept fresh
How Dynamic Rendering Works
The implementation has three components:
- User agent detection: At the web server or CDN level, check the incoming user agent against a list of known crawlers (Googlebot, Bingbot, Slurp, Baidu, etc.)
- Rendering service: Crawlers are proxied to a headless browser service (Rendertron, Prerender.io, or custom Puppeteer) that renders the page fully and returns static HTML
- Cache layer: Pre-rendered HTML is cached so the same URL doesn’t require a fresh render for every crawler request
Implementation: Rendertron (Open Source)
Rendertron is Google’s recommended open-source dynamic rendering solution. It uses Puppeteer (headless Chrome) to render pages and serve the resulting HTML to crawlers.
Step 1: Deploy Rendertron
# Deploy with Docker
docker run -p 3000:3000 ghcr.io/GoogleChrome/rendertron
# Or deploy to Google Cloud Run
gcloud run deploy rendertron --image ghcr.io/GoogleChrome/rendertron --platform managed --region us-central1 --allow-unauthenticated
Step 2: Configure Nginx for Dynamic Rendering
map $http_user_agent $prerender_ua {
default 0;
"~*googlebot" 1;
"~*bingbot" 1;
"~*yandexbot" 1;
"~*baiduspider" 1;
"~*twitterbot" 1;
"~*facebookexternalhit" 1;
"~*perplexitybot" 1;
"~*claudebot" 1;
"~*gptbot" 1;
}
server {
listen 80;
server_name yourdomain.com;
location / {
if ($prerender_ua = 1) {
rewrite .* /rendertron/$scheme://$host$request_uri? last;
}
# Serve your SPA normally for non-crawlers
try_files $uri $uri/ /index.html;
}
location /rendertron/ {
proxy_pass http://your-rendertron-service:3000/render/;
proxy_set_header X-Real-IP $remote_addr;
}
}
Step 3: Configure Caching
Without caching, every crawler request triggers a fresh render. Cache pre-rendered HTML for 24 hours (or until you deploy content changes):
proxy_cache_path /tmp/rendertron-cache levels=1:2 keys_zone=rendertron:10m max_size=1g;
location /rendertron/ {
proxy_cache rendertron;
proxy_cache_valid 200 24h;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_pass http://your-rendertron-service:3000/render/;
}
Implementation: Prerender.io (Managed SaaS)
For teams that want managed infrastructure, Prerender.io is the most widely deployed commercial dynamic rendering service. It handles the rendering, caching, and cache invalidation infrastructure.
Prerender.io Middleware (Node.js)
const prerender = require('prerender-node')
.set('prerenderToken', 'YOUR_TOKEN');
app.use(prerender);
Prerender.io automatically detects crawler user agents using an up-to-date list and routes them to the prerendered version. The middleware handles cache invalidation when you push content updates.
Verifying Dynamic Rendering Is Working
Test 1: User Agent Switching in Chrome DevTools
- Open DevTools → Network Conditions → User Agent → Custom
- Set user agent to:
Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) - Load your SPA page — you should see fully rendered HTML, not a JavaScript shell
Test 2: Google Search Console URL Inspection
Use URL Inspection on a JavaScript-heavy page. The rendered screenshot should show all content visible to users. If the screenshot now matches the full page, dynamic rendering is working for Googlebot.
Test 3: Curl with Googlebot User Agent
curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://yourdomain.com/product-page/ | grep -i "product-title"
If the product title appears in the curl output, the dynamic renderer is correctly serving HTML to crawlers.
Common Dynamic Rendering Mistakes
- Not including AI crawlers in the user agent list: GPTBot, ClaudeBot, PerplexityBot, and Googlebot-Extended all benefit from dynamic rendering. Add them to your detection list.
- Cache TTL too short: If pre-rendered HTML expires every hour, rendering load increases significantly. 24 hours is the standard; invalidate on deploy.
- Not invalidating cache on content updates: Stale pre-rendered HTML means crawlers see outdated content. Implement deploy hooks that clear the renderer cache on content changes.
- Blocking the rendering service from your own application: The headless browser making render requests must be able to reach your own CDN and static assets or the render will be incomplete.
Our technical SEO team diagnoses JavaScript rendering issues and implements the right solution — SSR, dynamic rendering, or hybrid — for your specific stack and business constraints.
FAQ: Dynamic Rendering for JavaScript SEO
What is dynamic rendering in SEO?
A server-side technique that detects crawler user agents and serves pre-rendered HTML to them while serving the JavaScript application to real users. Solves JavaScript indexing problems without requiring SSR refactoring.
Is dynamic rendering a Google-approved SEO practice?
Google has documented it as a viable workaround for JavaScript indexing limitations. It’s classified as a workaround, not a permanent solution — SSR is the preferred long-term approach.
What tools can implement dynamic rendering?
Rendertron (open-source), Prerender.io (SaaS), SEO4Ajax, and custom Puppeteer/Playwright solutions. Rendertron is Google-recommended; Prerender.io is the most common commercial option.
How do I know if my JavaScript content is being indexed?
Use Google Search Console’s URL Inspection tool — the rendered screenshot shows what Googlebot sees. Content missing from the screenshot is not being indexed.
Should I use dynamic rendering or SSR for my SPA?
SSR is the correct long-term choice. Dynamic rendering is a practical interim solution when SSR refactoring is expensive. New applications should use Next.js, Nuxt, or SvelteKit with SSR from the start.