JS Rendering and SEO: How the Two-Wave Index Really Works

JS Rendering and SEO: How the Two-Wave Index Really Works

Most posts about js rendering repeat a comforting half-truth: “Google renders JavaScript now, so don’t worry about it.” That’s the sentence that gets React and Vue sites quietly buried. Google can execute JavaScript — but it does so in a separate, deferred pass through a render queue, and everything that only exists after that pass runs is invisible to the index until Google gets around to it. The gap between what’s in your initial HTML and what’s in your rendered DOM is where rankings leak, and understanding that gap precisely is the whole job.

Crawling, Rendering, and Indexing Are Three Different Steps

The single most useful thing you can do is stop treating “Google sees my page” as one event. It’s three, and they happen at different times to different content:

  • Crawling — Googlebot fetches the raw HTML your server returns and parses it for links in href attributes to discover more URLs. No JavaScript has run yet.
  • Rendering — the URL is placed in a render queue, where a headless, evergreen Chromium instance (Google’s Web Rendering Service) executes your JavaScript and builds the final DOM.
  • Indexing — Google parses that rendered HTML for content and for any new links the JavaScript injected, then decides what to store.

Conflating these is the root cause of most js rendering failures. A page can be crawled and never rendered, rendered and never indexed, or indexed on its raw HTML long before the rendered version is processed. Each boundary is a place your content can fall through.

The “Two-Wave” Model, and What Actually Happens Now

Google introduced the two-wave framing at I/O 2018: a first wave indexes the raw HTML immediately, then a second wave revisits the page after rendering to pick up JavaScript-generated content. The mental model is still useful, but the literal “two waves” language is dated — Google now describes a single pipeline of crawling → rendering → indexing with rendering as a deferred stage rather than a distinct second crawl.

What hasn’t changed is the consequence. If your title tag, canonical, main body copy, or internal links only appear after JavaScript runs, they don’t exist during that first pass. Google’s own documentation is blunt about the wait: a page “may stay on this queue for a few seconds, but it can take longer than that.” For a small site the delay is often trivial. For a large, frequently-changing site with a heavy JavaScript payload, the render queue is a real bottleneck — this is exactly the render-budget problem that starves big single-page apps of timely indexing.

What Googlebot’s Renderer Will and Won’t Do

The WRS runs a current version of Chromium, so modern JavaScript, ES modules, and most APIs work. But it is not a patient human user, and the differences matter:

  • It does not scroll, click, hover, or type. Content behind a “load more” button, an infinite scroll that fires on scroll events, or a tab that only fetches on click is frequently never seen.
  • It does not persist state across page loads — no localStorage or cookies carried between renders, so anything gated on prior interaction is invisible.
  • It denies permission prompts (geolocation, camera, notifications) automatically; if your content depends on a granted permission, plan for the denial path.
  • It caches aggressively and unpredictably. The renderer may reuse stale JavaScript and CSS, so cache-bust with fingerprinted filenames when you ship changes.
  • It abandons slow resources. Requests that hang or scripts that error can leave you with a half-rendered DOM that gets indexed as-is.

The practical test is simple: if a user has to do something to reveal content, assume Googlebot won’t reveal it. Put anything you need indexed into the DOM on load, without interaction.

Client-Side Rendering: Why the Default Framework Setup Hurts

A vanilla create-react-app or a default Vue SPA ships an almost-empty HTML shell — often little more than <div id="root"></div> — and builds the entire page in the browser. Googlebot crawls that shell, sees no real content and no links, and has to wait for the render pass to find anything at all. Every discovery — every internal link, every new URL — is now blocked behind rendering. On a large site this compounds: pages get discovered slowly because the links pointing to them don’t exist until something renders, and rendering is exactly the constrained resource. Client-side-only rendering doesn’t usually cause a clean, obvious failure; it causes a slow, partial one that’s hard to diagnose.

Server-Side Rendering and Static Generation: The Durable Fix

The reliable answer is to make sure the content is in the HTML before it ever reaches Googlebot. Two approaches do this:

  • Server-side rendering (SSR) — the server runs your framework and returns fully-formed HTML for each request. Frameworks like Next.js, Nuxt, and SvelteKit make this the default path. Googlebot indexes real content on the first crawl; no render queue dependency for your core content.
  • Static site generation (SSG) — pages are pre-rendered to HTML at build time and served from a CDN. Fastest of all, ideal for content that doesn’t change per request.

Both then hydrate in the browser: the shipped HTML is immediately visible and indexable, and JavaScript attaches interactivity on top of it. This is the pattern Google actively recommends, and it also serves the growing set of AI crawlers and answer engines that don’t execute JavaScript at all. Server-rendered HTML is the lowest common denominator every bot can read.

Dynamic Rendering Is a Workaround, Not a Recommendation

You’ll still see advice to use dynamic rendering — detecting bots by user-agent and serving them a pre-rendered snapshot while users get the JavaScript app. Google explicitly stepped back from this: it’s now framed as a stopgap for specific limitations, not a long-term solution. The reasons are real. Maintaining two rendering paths is brittle, user-agent lists drift, and serving substantially different content to bots than to users sits uncomfortably close to cloaking. If you’re building or rebuilding, go straight to SSR or SSG. Reserve dynamic rendering for a legacy app you genuinely can’t re-architect yet.

How to Diagnose a JS Rendering Problem

Don’t guess — inspect the rendered output directly. The workflow that catches almost everything:

  • URL Inspection in Google Search Console. Run a live test, then open “View Crawled Page” and “Screenshot.” This shows the actual rendered HTML and DOM Google produced — the ground truth for what got indexed.
  • Compare raw vs rendered. View source (raw HTML, pre-JavaScript) against the rendered DOM in DevTools’ Elements panel. Content present in the DOM but missing from view-source is JavaScript-dependent — that’s your risk list.
  • Disable JavaScript in the browser and reload. Whatever disappears is what a non-executing crawler sees. If your headline, body, and nav vanish, so does your indexable content for many bots.
  • Check the render for silent errors. A single uncaught exception or a blocked resource in robots.txt (never disallow your JS/CSS bundles) can leave Googlebot with a broken partial render.

This is also where continuous monitoring beats a one-time check. SEO Rocket’s real-crawler site audit fetches your pages the way a bot does and flags the tells of a rendering problem — pages that come back thin or effectively blank, missing titles and canonicals, internal links that don’t resolve, and broken status codes — with the likely cause explained rather than just a red dot. It won’t replace GSC’s URL Inspection for a deep single-page render trace, but it surfaces the sitewide pattern automatically and on a schedule, so a regression from a new deploy shows up before it costs you traffic.

Common JS Rendering Mistakes That Tank Indexing

  • Blocking JavaScript or CSS in robots.txt. If Googlebot can’t fetch the bundle, it can’t render the page. Allow them.
  • Links that aren’t real links. A <div onclick> or a router-only <a> without a real href is not a crawlable link. Use genuine <a href> so URLs are discoverable in the raw HTML.
  • Injecting the canonical or meta robots via JavaScript. These can be missed or misread on the first pass. Put SEO-critical tags in the server-rendered HTML.
  • Soft 404s in a SPA. A client-side router that returns HTTP 200 for a missing route tells Google the empty page is valid. Return a real 404 status from the server.
  • Content that requires interaction. Tabs, accordions, and “read more” panels that only fetch on click hide their contents from the index.

Where JS Rendering Fits Your Wider Technical SEO

Rendering is one link in a chain: crawlability, render budget, indexation control, and Core Web Vitals all interact. Heavy JavaScript that solves your rendering problem can still wreck your Interaction to Next Paint (the Core Web Vital that replaced FID in March 2024, with a “good” threshold of 200ms or under). SSR fixes discoverability but shifts cost to your server and to hydration. The goal isn’t to eliminate JavaScript — it’s to make sure nothing a searcher or a crawler needs depends on JavaScript having run. Get the content into the HTML, keep the interactivity in the browser, and you’ve resolved the tension that sinks most js rendering setups. When you plan content around this, SEO Rocket’s keyword research and competitor gap analysis run on real Ahrefs data, and its validation-gated AI writer produces server-renderable articles — so the pages you ship are indexable by construction, not by luck.

Frequently Asked Questions

Does Google index JavaScript-rendered content?

Yes, Googlebot runs a current Chromium engine and indexes content generated by JavaScript. The catch is timing and reliability: rendering happens in a deferred queue that can take seconds to considerably longer, and content requiring scrolling, clicking, or a granted permission often isn’t seen at all. Anything you need indexed should appear in the DOM on load without interaction.

Is client-side rendering bad for SEO?

Not automatically, but it’s the riskiest default. Pure client-side rendering ships an empty HTML shell, so content and links only exist after the render pass — which slows discovery, strains render budget on large sites, and hides everything from bots that don’t run JavaScript. Server-side rendering or static generation with hydration removes that risk and is the recommended approach.

How can I see what Googlebot renders?

Use the URL Inspection tool in Google Search Console: run a live test, then open “View Crawled Page” and the screenshot to see the exact rendered HTML and visual output. Cross-check by comparing view-source (raw HTML) against the rendered DOM in DevTools, and by reloading with JavaScript disabled to see what a non-executing crawler gets.

Should I still use dynamic rendering?

Only as a temporary workaround for a legacy app you can’t re-architect yet. Google no longer recommends it as a long-term strategy because maintaining separate bot and user paths is brittle and risks drifting into cloaking. For new builds, go straight to server-side rendering or static generation.

Questions? Chat with us