Modern websites, ranked in AI search✦Cited by ChatGPT, Perplexity & Google AI Overviews✦32% of our own traffic comes from AI search✦Modern websites, ranked in AI search✦Cited by ChatGPT, Perplexity & Google AI Overviews✦32% of our own traffic comes from AI search✦Modern websites, ranked in AI search✦Cited by ChatGPT, Perplexity & Google AI Overviews✦32% of our own traffic comes from AI search✦Modern websites, ranked in AI search✦Cited by ChatGPT, Perplexity & Google AI Overviews✦32% of our own traffic comes from AI search✦
AEO & AI Search

JavaScript SEO: what Googlebot actually renders

Googlebot renders JavaScript in a second pass with an evergreen Chromium. Where SPAs still break, and how to ship a JS site that gets indexed

Mahmoud Halat·May 26, 2026·7 min read
On this page
  1. The two-pass rendering model
  2. Why the "Google can't see my SPA" panic is mostly wrong
  3. Three patterns that quietly kill indexing:
  4. How browsers (and Googlebot) actually parse HTML
  5. Use the History API, not URL fragments
  6. The canonical-via-JavaScript trap
  7. SPA-heavy verticals where this matters most
  8. The soft-404 problem in SPAs
  9. What to check before you call it done
  10. Sources

Key Takeaway

Googlebot renders JavaScript using an evergreen Chromium, but in a second pass after the initial crawl. Pages with empty initial HTML, content gated behind user interaction, or JavaScript-injected canonicals that conflict with the source HTML are the failure modes most likely to break indexing.

Googlebot does render your JavaScript, just in a second pass, on its own schedule, with a stripped-down Chromium that carries no cookies and waits only so long before moving on. That second pass is where most JavaScript SEO problems live.

I have audited dozens of React and Vue and Next.js sites for clients who were convinced "Google can't see JavaScript." That hasn't been true since 2019, when Google switched to an evergreen Googlebot running modern Chromium. The actual problem is more boring and more fixable: the site ships an empty initial HTML, then asks Googlebot to wait. Sometimes Googlebot waits, sometimes it doesn't.

This post is what the Google Search Central docs and recent Search Off the Record episodes actually say about rendering, and what to fix on a JS-heavy site.

The two-pass rendering model

Per Google's JavaScript SEO basics, Googlebot processes a JS-powered page in three phases: crawl, render, index. Crawl and render are not the same step.

In phase one, Googlebot fetches the URL and reads the response, parsing any HTML it gets, looking for href attributes to add to its crawl queue, and checking the robots meta tag. If the initial HTML is empty (just a <div id="root"> and a script tag) there is nothing here to index yet.

In phase two, every 200-response page goes into a render queue where a headless Chromium picks it up, executes the JavaScript, and produces the rendered HTML. Google parses that HTML for links and content, then indexes it.

> Googlebot queues every page with a 200 status code for rendering, regardless of whether the page actually uses JavaScript. The page may sit in that queue for seconds, or longer.

The render queue is the bottleneck nobody talks about. If your page only has content after JavaScript runs, you are betting on that queue. For a small site, that bet usually wins. For a fast-changing site (listings, real-time inventory, news) that bet costs you indexing latency.

Why the "Google can't see my SPA" panic is mostly wrong

The old fear came from a real era. Per Martin Splitt and Edu Pereda on Search Off the Record episode 58, the original "Google can't render JS" sentiment dates back to when rendering was slow and unreliable. Since the evergreen switch, the rendering engine is current Chromium with the full set of standard browser APIs.

What it does not support, per Google's troubleshooting guide: user-permission features (camera, geolocation), state across page loads (localStorage and cookies are wiped per fetch), and non-HTTP transports (WebSockets, WebRTC). If your content depends on any of those, the rendered HTML will be empty.

That is the real failure mode. The problem isn't "JavaScript broken," it's "content gated behind something Googlebot literally cannot do."

Three patterns that quietly kill indexing:

  • Content that only loads after a click ("Load more," "Show details," tabbed interfaces with no DOM-level content)
  • Content that requires a cookie set on the previous page
  • Content fetched from an API that fails the second request because the session is gone

If a real human can see the content on page load with no interaction, Googlebot probably can too. If they have to click first, Googlebot does not.

How browsers (and Googlebot) actually parse HTML

Per Gary Illyes and Martin Splitt on SOTR episode 105 ("How Browsers Really Parse HTML"), there is a parsing rule most developers do not know: a non-metadata element appearing in the <head> implicitly closes the head and opens the body. Anything after that point (your canonical, your hreflang, your robots meta) ends up inside the body and is silently ignored.

The pattern that triggers this in the wild: an inline script in the head that injects an iframe or a paragraph during render. Gary cited a real example where hreflang tags appeared correct in source but were ignored because an injected iframe had already closed the head.

> Metadata elements (<link>, <meta>, <title>) only count when they are inside the head. A stray <p> or <iframe> in the head terminates the head context, and any subsequent metadata is treated as body content and discarded.

This is one of the strongest arguments for putting your canonical, hreflang, and robots meta into the server-rendered HTML rather than injecting them later via JavaScript.

Use the History API, not URL fragments

A single-page app that routes by URL fragment (example.com/#/products) is a dead end for indexing. The AJAX crawling scheme was deprecated in 2015; Googlebot does not resolve fragment URLs to distinct pages.

The fix is the History API. Real URLs in href attributes, intercepted in JavaScript, with window.history.pushState updating the visible URL. The Google docs example is short and worth copying.

The same rule applies to lazy-loaded content. Use loading="lazy" for images and iframes. For content blocks, use the IntersectionObserver API to load when the element enters the viewport. Do not rely on scroll or click handlers, because per Google's lazy-loading guidance, Search does not interact with your page. It doesn't scroll or click.

For paginated content under infinite scroll: give each chunk a real, persistent URL (?page=12, not ?date=yesterday), link to those URLs sequentially, and update the visible URL via History API as new chunks become primary. That is the only way a 200th-position result becomes individually crawlable.

The canonical-via-JavaScript trap

This is the most common JS SEO bug we see when we audit client sites. The HTML ships with one canonical. JavaScript runs and injects a different canonical. Now Google has two signals pointing in opposite directions.

Per Martin Splitt on SOTR 105: "If you have a canonical that is there at the first time we fetch the HTML from the server and then the JavaScript changes it, we actually advise against doing that." The fix is simple to state and easy to skip: set the canonical in the server HTML, or set it only in JavaScript, never both and never conflicting.

If your CMS or React framework supports server-side rendering of the canonical link tag, use it. If it does not, ship empty in HTML and inject from JS only.

SPA-heavy verticals where this matters most

Two industries we work with hit JS SEO walls constantly. Real estate sites lean on map components and filter UIs that load listings client-side after a user interaction, so Googlebot sees an empty grid. AV installers ship portfolio galleries built as carousels with content lazy-loaded behind clicks, then wonder why their case studies do not rank.

For both, the fix is usually the same: server-render the content card body. Keep the fancy interaction client-side. The HTML response should answer the question "what's on this page" before any JavaScript runs.

The soft-404 problem in SPAs

Client-side routing breaks HTTP status codes. A user navigates to /product/this-was-deleted, the SPA loads, the API returns "not found," and the page renders an error state, but the HTTP status is still 200. Google indexes it as a real page. Now you have ten thousand thin error pages in the index.

Per Google's docs, two fixes work: either JavaScript-redirect to a URL that returns a real 404 from the server (window.location.href = '/not-found'), or dynamically inject a <meta name="robots" content="noindex"> when the API says the resource is gone. Either one stops the bleed, so pick whichever your stack makes easier.

What to check before you call it done

Use the URL Inspection Tool in Search Console or the Rich Results Test. Both run the page through Google's actual rendering pipeline and show the post-render HTML. If your content is not in that rendered HTML, Google will not index it. No amount of theorizing fixes this, so open the tool and look at what came back.

While you are there: check the JavaScript console output the tool exposes. If something threw an error during render, that is where you find out. Logging window.onerror to your analytics is the move; per Google, that catches most runtime issues. Parse errors will not show up, but the rest will.

JavaScript SEO comes down to two things you can verify in the URL Inspection tool: the initial HTML gave Google enough to crawl with, and the rendered HTML gave Google everything it needed to index. If either one comes back short, you have your fix list.

Sources

Is your site invisible to AI search?

Get a free AEO infrastructure audit and find out what your competitors are doing that you're not.

Get Your Free Audit
Quick answers

Frequently asked.

Yes, it does. Googlebot runs an evergreen version of Chromium and renders JavaScript on every page that returns a 200 status code. Rendering happens in a second pass after the initial crawl, so JS-loaded content may take longer to be indexed than server-rendered HTML.

Google can index SPAs that ship meaningful content in the initial HTML or load content without requiring user interaction. SPAs that use URL fragments for routing, gate content behind clicks, or depend on browser features Googlebot does not support (cookies across loads, geolocation) will not index correctly.

Avoid it when possible. If the server HTML already has a canonical, do not change it with JavaScript, because that creates conflicting signals. The safest pattern is to set the canonical in the server-rendered HTML and never modify it client-side. If JS is your only option, ship no canonical in HTML.

No. Googlebot does not interact with your page. It does not scroll, click, hover, or trigger any user-action events. Any content loaded only on scroll or click will not be seen. Use the IntersectionObserver API and native loading=lazy for images, and ensure all content is reachable without interaction.

Google fetches your page once and parses the initial HTML, then queues the page for a second pass where headless Chromium executes the JavaScript and produces the rendered HTML. Only after that second pass does the page enter the index with its JS-generated content.

Written by

Mahmoud Halat

Space & Story builds the search and AEO infrastructure these posts describe.

More articles

Share this article

Work with us

Find out what AI search engines see when they look at your site — and what your competitors are doing that you aren't.

Get a free audit