Dynamic Rendering & Bot Prerendering

Prerendering executes your single-page app in a headless browser and caches the resulting HTML so that a crawler receives fully populated markup instead of an empty shell. Dynamic rendering is the variant that serves that snapshot specifically to bots while users continue to get the live app. Both let a client-rendered application become crawlable without migrating to server-side rendering, which is why they remain a pragmatic option inside the broader prerendering and SSR strategies toolkit — even though Google now frames dynamic rendering as a workaround rather than a destination.

Prerequisites

  • A CSR app whose public routes are deterministic from the URL (no auth required to see indexable content).
  • A prerendering mechanism: a build-time prerenderer, a self-hosted service, or edge middleware that can run headless Chromium.
  • The ability to branch on the request — by user agent for dynamic rendering, or to prerender for everyone.
  • A canonical list of routes to prerender (usually derived from your sitemap).
Three places a prerendering mechanism can run headless Chromium The key prerequisite is a prerendering mechanism, available in three forms — a build-time prerenderer, a self-hosted service, or edge middleware — all producing the same prerendered HTML from a canonical list of routes. Pick one place to run headless Chromium Build-time snapshots baked at deploy from the sitemap Self-hosted service renders on demand, caches the result Edge middleware renders and branches by request at the CDN Prerendered HTML snapshot from a canonical list of routes
Whichever of the three mechanisms you choose, all run headless Chromium over the same route list to produce the prerendered HTML a crawler receives.

How it breaks

Two failure shapes dominate. First, the snapshot and the live app drift: a deploy changes the client app but the prerender cache is not invalidated, so crawlers index stale titles, prices, or structured data. Second, the bot-detection branch misfires — a new crawler user agent is not matched, so it receives the empty SPA shell and indexes nothing.

Dynamic rendering request flow An incoming request is classified as bot or user; bots receive a cached prerendered HTML snapshot while users receive the client-side app bundle. Incoming request Bot user agent? Cached prerendered HTML snapshot yes Client-side app bundle (live) no
Dynamic rendering branches on the request: bots get a pre-executed HTML snapshot, users get the live SPA. Keep the two in sync to avoid cloaking.

Step-by-step fix

  1. Prefer prerendering for everyone over user-agent branching. Serving the same prerendered HTML to users and bots eliminates cloaking risk and the divergence problem. Only fall back to user-agent detection when the app cannot tolerate a static first paint.

  2. Generate snapshots from your sitemap. Drive the prerenderer from the canonical URL list so coverage matches what you submit to search engines. The React prerendering walkthrough shows a build-time setup.

  3. Invalidate the cache on deploy and on data change. Tie snapshot regeneration to your deploy pipeline and to content updates so the cache never outlives the live content.

    // Edge middleware: serve a fresh-enough snapshot, regenerate when stale
    const MAX_AGE = 60 * 60; // 1 hour
    async function handle(request, cache) {
      const snap = await cache.get(request.url);
      if (snap && snap.age < MAX_AGE) return snap.html;
      const html = await prerender(request.url);   // headless render
      await cache.put(request.url, { html, age: 0 });
      return html;
    }
  4. Verify metadata survives the snapshot. The prerendered HTML must contain the final title, canonical, and JSON-LD, not the pre-update defaults — align with avoiding metadata hydration pitfalls.

Steps 3 and 4 hinge on the snapshot never outliving the content. Model the cache as a small state machine: a snapshot is fresh until a deploy or content change invalidates it, at which point a re-render returns it to fresh before the next crawl arrives.

Prerender snapshot cache as a state machine A fresh snapshot serves bots on a cache hit; a deploy or content change marks it stale; a headless re-render regenerates it and returns the state to fresh. Keep the snapshot fresh: invalidate on deploy and on data change Fresh snapshot served to bots Stale drifts from live app Regenerating headless re-render re-render render complete deploy / content change cache hit
Tie re-render to deploys and content edits so the snapshot returns to fresh before a crawler ever reads the stale copy.

Gotchas & edge cases

  • Snapshot timeout. If the prerenderer captures the DOM before async data resolves, it caches a half-rendered page. Wait for a render-complete signal, not a fixed timeout.
  • Cloaking drift. Any content the bot snapshot includes but users never see (or vice versa) risks a cloaking penalty; audit parity regularly.
  • Infinite or query-dependent routes. Prerendering every filter combination explodes the cache; canonicalize filtered views instead of snapshotting them all.
  • Relying on dynamic rendering long-term. Google recommends migrating to SSR or hydration over time; treat dynamic rendering as a bridge. See whether dynamic rendering is still recommended.
Fixed timeout versus a render-complete signal when snapshotting A fixed timeout can capture the DOM before async data resolves and cache a half-rendered page, while waiting for a render-complete signal captures the fully populated snapshot. The snapshot-timeout trap: wait for the signal, not the clock Fixed timeout — capture at 2s capture @2s data ready @3s DOM grabbed too early caches a half-rendered page, missing prices and structured data Wait for render-complete signal data ready capture on signal DOM grabbed after data caches the fully populated page, complete metadata and content
The most common of these edge cases: a fixed timeout can fire before async data resolves, so gate the capture on a render-complete signal instead of the clock.

Validation checklist

Sitemap URLs and prerendered URLs must be the same set Two overlapping sets: URLs in the sitemap but not prerendered ship an empty shell to bots, URLs prerendered but not in the sitemap are orphan snapshots, and validation passes only when the two sets are equal. Validation passes when both sets are identical Sitemap URLs not prerendered = empty shell to bots Prerendered URLs not in sitemap = orphan snapshot indexable overlap Goal: the two sets are equal — no left-only, no right-only
Any URL in the sitemap but not prerendered serves bots an empty shell, and any prerendered but absent from the sitemap is an orphan — the check passes only when the sets match exactly.

Performance & crawl-budget notes

Because a snapshot is plain HTML, bots index it in the first wave and never spend render-queue budget on the route — the same indexing-velocity win as SSR, achieved without rebuilding the app. The cost moves to your prerender infrastructure: each cache miss runs a headless render, so cache hit rate and invalidation strategy determine whether prerendering is cheap or expensive at scale.

The two lanes below show why the snapshot indexes faster: plain HTML is content-complete in the first response, while a raw CSR route must queue for the render service before its content exists to index.

Prerendered snapshot versus raw CSR route on the indexing timeline A prerendered snapshot is indexed in the first wave immediately, while a raw CSR route is crawled, held in the render queue, then indexed in a delayed second wave. Snapshot indexes in the first wave; raw CSR waits for the render queue time to index Prerendered snapshot Indexed first wave · immediate Raw CSR route crawl HTML wait in render queue indexed · second wave
The snapshot is content-complete on the first response, so it skips the render queue that delays a raw CSR route to a later indexing wave.

Go deeper

Two directions from dynamic rendering: build it or reassess it A signpost pointing two ways from this guide: one path to a concrete React prerendering build, the other to Google's current guidance on whether dynamic rendering is still recommended. Two ways to go deeper from here This guide dynamic rendering Build it React prerender setup Reassess it Google's current guidance one path implements the bridge, the other asks how long to keep it
From here you can either implement the prerender bridge for a React app or step back to Google's current guidance on how long to lean on dynamic rendering.

Frequently Asked Questions

Is dynamic rendering considered cloaking? Not if the prerendered HTML matches the content users see. Cloaking is serving materially different content to crawlers than to users. Dynamic rendering serves the same content in a pre-executed form, so it is allowed — but it drifts toward cloaking if the snapshot and the live app diverge.

Do I still need prerendering if Google renders JavaScript? Google renders JavaScript, but rendering is queued and capped by an execution budget, and other crawlers and social scrapers render little or no JavaScript. Prerendering guarantees content and metadata in the first response for every bot, which is why it remains useful for CSR apps that cannot move to SSR.

← Back to Prerendering & SSR Strategies