🧩

JavaScript SEO & Rendering

How Google renders JavaScript — and how to choose SSG, SSR, ISR, or dynamic rendering for SEO.

📖 12 min read 🕑 Updated 2026-06-22

Modern front-end frameworks ship most of their content as JavaScript that runs in the browser. That is fantastic for product engineering and terrible for the naive mental model of SEO, where a crawler downloads HTML and reads it. Google does execute JavaScript — but it does so on its own timeline, with its own budget, and with failure modes that quietly cost you rankings. If your most important content only exists after a fetch() resolves, you are betting your organic traffic on a render that may be delayed by days or may never complete the way you expect.

This guide explains what actually happens inside Google’s rendering pipeline, compares the rendering strategies you can choose at build time, walks through the pitfalls that bite real apps, and gives you a debugging workflow you can run today.

How Google renders

It helps to think of indexing as a two-wave process, not a single pass.

  1. Crawl. Googlebot requests the URL and gets back the initial HTML — exactly what curl https://example.com would return. Links discovered here go into the crawl frontier. If your initial HTML is an empty <div id="root"></div>, this wave sees almost nothing.
  2. Render. The URL is placed in the Web Rendering Service (WRS) queue. WRS is a headless, evergreen Chromium. When capacity is available, it loads the page, executes JavaScript, waits for the network to settle, and produces the rendered HTML (the post-JavaScript DOM).
  3. Index. Google indexes the rendered HTML and extracts any new links it found there for the next crawl cycle.

The critical insight is the gap between wave 1 and wave 2. Rendering is deferred because executing JavaScript is roughly an order of magnitude more expensive than fetching HTML. In practice the delay is often minutes, but it can stretch to days when crawl demand is high or your site has a large render queue.

              CRAWL                  RENDER (WRS queue)            INDEX
  Googlebot ──► raw HTML ──► [ wait: minutes → days ] ──► headless
                  │                                        Chromium
                  ▼                                           │
            links found                                  rendered DOM
            in raw HTML                                       │
                  └──────────────► next crawl cycle ◄── new links found

This deferral interacts with two budgets you do not control directly:

  • Crawl budget — how many URLs Googlebot fetches from your origin in a window. Driven by site authority, server speed, and error rate.
  • Render budget — roughly, how much CPU/time WRS is willing to spend rendering your pages. Slow, script-heavy pages consume more of it, so fewer of your pages get rendered per cycle.

⚠️ Note: The classic failure is content that only exists after client-side injection. The crawl wave sees an empty shell, and if the render wave is delayed or your script errors out under WRS, the content is never indexed. “It works in my browser” is not evidence that it works for Googlebot — your browser is not rate-limited, sandboxed, or running weeks-old cached responses.

A few WRS specifics that surprise developers:

  • WRS does not scroll, click, or move a mouse. Anything gated behind user interaction is invisible to it.
  • WRS denies permission requests (geolocation, camera, notifications) automatically. Code that waits on those prompts hangs.
  • WRS aggressively caches resources (JS, CSS, API responses) on its own schedule, which can be much longer than your HTTP cache headers suggest. A “fresh deploy” may be rendered against stale assets.
  • WRS uses a current Chromium, so modern JS syntax is fine — but there is no localStorage/sessionStorage persistence you can rely on across renders, and cookies are not carried like a logged-in user’s.

Rendering strategies

Where you generate the HTML determines how much of this risk you carry. Here are the five strategies you will actually choose between.

StrategyWhen HTML is builtCrawler sees content immediately?SEO friendlinessBest for
CSR (client-side render)In the browser, after JS runsNo — empty shell first⚠️ RiskyLogged-in dashboards, app-like UIs behind auth
SSR (server-side render)Per request, on the serverYes✅ StrongPersonalized or frequently changing public pages
SSG (static site generation)At build timeYes✅✅ StrongestDocs, blogs, marketing, anything stable
ISR (incremental static regen)At build, then regenerated on a schedule/on-demandYes (serves last good static)✅✅ StrongLarge catalogs that change but not per-request
Dynamic renderingPer request, bot gets pre-rendered HTML, users get CSRYes (for bots)⚠️ WorkaroundLegacy SPAs you cannot rewrite

A few notes that the table can’t carry:

  • CSR is the default for a plain create-react-app or unconfigured SPA. It is the strategy that creates almost every JavaScript-SEO problem in this guide. It is perfectly fine behind a login where there is nothing to index.
  • SSR sends fully-formed HTML on the first request, then “hydrates” it into an interactive app. The crawler gets real content in wave 1, so rendering deferral stops mattering. The cost is server compute and latency per request.
  • SSG pre-renders every page to a static .html file at build time. There is no render to wait for and no server to be slow — the crawler reads the same file a CDN serves. This is the gold standard for content sites.
  • ISR is SSG that can refresh: pages are static, but a page can be regenerated in the background after N seconds or on demand (e.g., Next.js revalidate). Crawlers always get a valid static page; freshness lags slightly. Ideal when you have 50,000 product pages and rebuilding all of them on every price change is infeasible.
  • Dynamic rendering sniffs the user agent and serves a pre-rendered snapshot (via something like Prerender.io or Puppeteer) to bots while real users get the SPA. Google now calls this a workaround, not a recommendation — it adds a fragile UA-detection layer and a second rendering pipeline to maintain. Reach for it only when rewriting the app is off the table.

🧑‍💻 Developer view: Think of it as who pays the rendering cost. CSR makes the user’s device and Google’s WRS pay it (twice). SSR makes your server pay it on every request. SSG pays it once at build time and never again. The further left you push that cost — toward build time — the less of Google’s render budget you spend and the fewer ways the render can fail.

Pitfalls

These are the issues that show up in real audits, in rough order of how often they cause silent traffic loss.

1. Client-side routing that never updates <head>. In an SPA, navigating between routes swaps the DOM without a full page load. If your router doesn’t also update the title, meta description, and canonical, every route inherits whatever was in the initial HTML. Google ends up indexing ten pages that all claim to be your homepage.

// ❌ Title and canonical are stale on every soft navigation
router.push('/pricing'); // DOM changes, <head> does not

// ✅ Update head metadata on every route change
router.afterEach((to) => {
  document.title = to.meta.title;
  document
    .querySelector('link[rel="canonical"]')
    .setAttribute('href', `https://example.com${to.path}`);
});

2. Lazy-loaded content that requires scroll or interaction. Infinite scroll, “load more” buttons, and content that mounts on IntersectionObserver are invisible to WRS because it does not scroll or click. If your article body or product list only appears after a scroll event, it is not in the rendered DOM Google indexes.

3. Hash-based routing. URLs like example.com/#/products/42 are a relic of old SPAs. Everything after # is a fragment — by spec, it’s never sent to the server and Google treats it as the same URL as example.com/. Hash routes collapse your entire site into one indexable page. Always use the History API (pushState) so routes are real, server-resolvable paths.

4. Timeouts and render-blocking work. WRS will abandon a render that takes too long. A waterfall of dependent fetch() calls, a 4 MB bundle, or a third-party script that hangs can push content past the cutoff. Slow renders also burn render budget, so fewer of your pages get rendered at all.

5. Content gated behind user interaction or consent. A cookie-consent wall that blocks rendering until “Accept,” tabs whose content only loads on click, or copy revealed by a “Read more” button — WRS clicks none of these. If it matters for SEO, it must be in the DOM without interaction.

6. Soft 404s and JS-driven error states. When an SPA can’t find a resource, it often renders a “Not found” message but still returns HTTP 200. Google sees a successful response with thin content and may index the error page. Return a real 404/410 status from the server, or use a noindex for genuinely missing content.

Best practices

The throughline: get critical content and metadata into HTML that exists before JavaScript runs, and treat JavaScript as enhancement.

  • Render critical content server-side or at build time. Headlines, body copy, primary nav, product details, and prices belong in the initial HTML via SSR or SSG. Let JavaScript enhance an already-complete page rather than create it.
  • Practice progressive enhancement. The page should convey its core meaning with JS disabled. Links should be real <a href> elements (WRS follows <a href>, not onClick handlers that call router.push). Buttons that navigate should be links.
  • Use the History API and update metadata per route. Every route needs its own title, meta description, and self-referencing canonical, updated on navigation. A framework head manager (react-helmet-async, @unhead/vue, Next/Astro’s built-ins) handles this cleanly.
// Per-route head with a framework-agnostic shape
function setSeoHead({ title, description, canonical }) {
  document.title = title;
  upsertMeta('description', description);
  upsertLink('canonical', canonical);
}
  • Put structured data in the HTML, not behind a delayed fetch. JSON-LD should be present in the rendered DOM as early as possible. SSG/SSR emit it directly; if you must inject it client-side, do it synchronously on mount, never after an async data round-trip.
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "JavaScript SEO & Rendering",
  "datePublished": "2026-06-22"
}
</script>
  • Keep JS lean and resilient. Code-split, defer non-critical scripts, and make sure a single third-party failure doesn’t blank the page. Self-host or async analytics and tag managers so they never block content.
  • Don’t block JS/CSS in robots.txt. WRS needs to fetch your scripts and styles to render the page. Blocking /_next/ or /assets/ means Google renders a broken page.

💡 Tip: A fast sanity check for any page — run curl -s https://your-url | grep "your headline text". If your main content isn’t in that output, the crawl wave can’t see it and you’re relying entirely on the deferred render wave. That’s the line between SSG/SSR (content present) and CSR (content absent).

Debugging

You don’t have to guess what Google sees — its tools show you the rendered output directly.

  • URL Inspection in Google Search Console. Inspect a live URL, then open View Tested Page → HTML. This is the rendered HTML as WRS produced it. Search it for your headline, your canonical, your JSON-LD. If they’re missing here, they’re missing from the index. The Screenshot and More info → Page resources tabs reveal blocked scripts and console errors that broke the render.
  • The site: operator. site:example.com/pricing or site:example.com "exact phrase from the page" tells you whether a page (and specific JS-injected text) is actually indexed. No results for a phrase that’s only in your rendered DOM is a strong signal the render isn’t landing.
  • Compare raw vs. rendered DOM yourself. view-source: (or curl) shows the crawl-wave HTML. DevTools → Elements shows the post-JS DOM. The diff between them is exactly the content that depends on a successful render — minimize it for anything SEO-critical.
  • Simulate Googlebot’s constraints. In Chrome DevTools, disable JavaScript (Command Menu → “Disable JavaScript”) and reload: that approximates the crawl wave. Throttle the network and CPU to surface renders that would time out under WRS load. The free Rich Results Test and Mobile-Friendly Test also render with Google’s own engine and report rendering errors.

⚠️ Note: Always test the live URL, not Google’s cached version, when validating a fix — the cache reflects the last successful render, which may predate your deploy by days.

From here

JavaScript rendering is fundamentally a decision you make in your build layer — the framework and output mode you pick determine whether you ever face these problems at all. Dig into those trade-offs in the Build layer guide.

This very site sidesteps the entire category of issues by using Astro with static generation: every page is pre-rendered to HTML at build time, so the crawl wave sees complete content, structured data, and metadata with zero reliance on the deferred render queue. That’s not an accident — it’s the most SEO-friendly default available, and it’s why content sites should reach for SSG first.

Key takeaways

  • ✅ Google renders JavaScript in a deferred second wave (WRS); never assume injected content is indexed promptly — or at all.
  • ✅ Push rendering cost toward build time: SSG is strongest, SSR strong, CSR risky for anything you want indexed.
  • ✅ Update title, meta description, and self-referencing canonical on every client-side route change.
  • ✅ Keep critical content and JSON-LD in HTML that exists before JS runs; WRS won’t scroll, click, or grant permissions.
  • ✅ Verify with GSC URL Inspection (rendered HTML), the site: operator, and a JS-disabled reload — test the live URL, not the cache.