Overview

Googlebot renders JavaScript, but it renders on a budget and with delay. A page whose content is injected by client-side JS sits in a deferred render queue that can take hours to days to clear, during which the indexed version is the empty HTML shell. Other crawlers (Bingbot, most AI bots, social previewers) render less JS or none at all. The rule for any page that needs to rank: ship the meaningful content in the initial HTML response. This page covers the rendering choices, the debugging tools, and the SPA patterns that break crawlers.

Pick the rendering mode that matches the page’s job

Four rendering modes; three are SEO-safe and one is not.

  • Static (SSG): HTML generated at build time. Best for content that changes on deploy. Astro, Next.js output: "export", Quartz, Hugo.
  • Server-side render (SSR): HTML generated per request on the server. Best for personalized or frequently-updated content. Next.js App Router, Remix, Astro output: "server", Nuxt.
  • Dynamic rendering: serve pre-rendered HTML to bots, client-rendered HTML to users. Deprecated by Google in 2024 as an anti-pattern. Avoid.
  • Client-side render (CSR): HTML shell + JS that injects content. Acceptable only for authenticated, interactive surfaces that do not need to rank.

Pair the choice with technical for the canonical and sitemap mechanics, and with core-web-vitals for the performance side.

Understand the render budget

Googlebot’s pipeline is two passes: an initial HTML fetch, then a queued render pass that executes JS. The render pass runs on a budget shared across the whole site.

  • The render queue can take from minutes to days; on large sites, days is normal.
  • During the gap, only the initial HTML is indexed. If the initial HTML is empty, the page is empty in the index.
  • Pages that fail to render in the budget window are dropped and retried later. Sites with high JS cost see fewer pages indexed.
  • AI bots (GPTBot, ClaudeBot, PerplexityBot) often do not render JS at all. If the initial HTML is empty, the page is invisible to AI search.

The fix is to put the content in the first HTML response, regardless of which framework ships it.

Watch hydration delay; it tanks LCP

Hydration is when the client JS attaches to the SSR’d HTML. Until hydration finishes, interactions queue and INP measurements stack up. Large hydration payloads also delay LCP because the browser blocks rendering on the JS.

  • Split client bundles per route. Do not ship the entire app bundle on the landing page.
  • Use islands or partial hydration where the framework supports it (Astro islands, React Server Components in Next.js App Router).
  • Defer non-critical JS with <script defer> or dynamic import. The LCP image and its container must not depend on JS.

Aim for the LCP element to be in the static HTML and styled by CSS alone. Hydration can finish after LCP; it cannot finish before.

Avoid the four SPA patterns that break crawlers

Single-page apps fail SEO most often through these four patterns.

  • Client-only routing without server fallback. Hash routes (/#/article/1) are not crawlable. Use the History API and configure the server to return the correct HTML for every path.
  • Content behind a click or scroll. Tabs, accordions, and “load more” buttons that fetch content on interaction hide it from the initial render. Ship the content in the HTML; hide with CSS if it should not be visible.
  • Lazy-loaded content above the fold. loading="lazy" on the LCP image delays LCP. Use fetchpriority="high" on the LCP image and lazy-load only below-the-fold content.
  • JavaScript-only navigation. <a onclick="..."> without an href is not a link to a crawler. Use real <a href> elements; intercept the click in JS if needed.

The pattern check: load the page with JS disabled. If the content and the nav both work, the SEO surface works.

Debug with URL Inspection and view-source

The two tools that show whether the crawler sees what you see.

  • Google Search Console URL Inspection: “Test live URL” returns the rendered HTML and a screenshot of what Googlebot sees. The gap between the rendered HTML and your browser’s DOM is the bug.
  • View-source vs the rendered DOM: view-source: in Chrome shows the initial HTML; DevTools shows the post-JS DOM. If a paragraph is in the rendered DOM but not in view-source, it is client-rendered.

Run both on every page template before launch. The view-source check catches client-only content; the URL Inspection check catches Googlebot-specific rendering issues (blocked resources, render timeouts).

# Quick check from the terminal
curl -A "Googlebot" https://example.com/page | grep -c "<main>"
# Zero means the main content is not in the initial HTML.

Common errors

  • Trusting that “Google renders JS” means JS-heavy pages rank fine. They rank later, slower, and less often.
  • Blocking .js files in robots.txt. Googlebot needs the JS to render; blocking it produces empty rendered HTML.
  • Shipping a 1 MB client bundle for a content page. The render budget bites, INP suffers, and the page underperforms versus the same content as static HTML.
  • Forgetting the social and AI previewers. og:image, og:title, and og:description must be in the initial HTML; renderers that do not run JS will miss them.
  • Treating dynamic rendering (UA sniffing to serve different HTML) as a fix. It is now a Google policy violation; serve the same HTML to everyone.