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 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. 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." 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. 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 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 ( 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. A single-page app that routes by URL fragment ( The fix is the History API. Real URLs in The same rule applies to lazy-loaded content. Use For paginated content under infinite scroll: give each chunk a real, persistent URL ( 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. 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. Client-side routing breaks HTTP status codes. A user navigates to Per Google's docs, two fixes work: either JavaScript-redirect to a URL that returns a real 404 from the server ( 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 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. Previous in series Core Web Vitals in 2026: what actually changes your score Next in series Canonicalization: stop fighting Google's canonical picker 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. 6 links · External Google Search Central Understand JavaScript SEO Basics Google Search Central Fix Search-Related JavaScript Problems Google Search Central Fix lazy-loaded content Google Search Central Blog The new evergreen Googlebot Search Off the Record How Browsers Really Parse HTML (SOTR 105) Google Search Central Blog Deprecating AJAX crawling scheme AEO & AI Search Googlebot fetches up to 2 MB of HTML per URL for Search. Anything past that is silently truncated, and most sites have no idea AEO & AI Search Three metrics. Three thresholds. One 75th-percentile rule most teams misread. Here's what actually changes Core Web Vitals on a real site AEO & AI Search rel=canonical is a hint, not a directive. How Google actually picks the canonical URL, and the three signals that reliably move the decisionhref attributes to add to its crawl queue, and checking the robots meta tag. If the initial HTML is empty (just a Why the "Google can't see my SPA" panic is mostly wrong
Three patterns that quietly kill indexing:
How browsers (and Googlebot) actually parse HTML
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., , ) only count when they are inside the head. A stray or in the head terminates the head context, and any subsequent metadata is treated as body content and discarded.Use the History API, not URL fragments
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.href attributes, intercepted in JavaScript, with window.history.pushState updating the visible URL. The Google docs example is short and worth copying.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.?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
SPA-heavy verticals where this matters most
The soft-404 problem in SPAs
/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.window.location.href = '/not-found'), or dynamically inject a 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
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.Sources
Further Reading
Industry sources we cite.
Frequently asked.
Continue with.
The 2MB rule: how Googlebot truncates your page
Core Web Vitals in 2026: what actually changes your score
Canonicalization: stop fighting Google's canonical picker