JavaScript SEO & Rendering
How Google renders JavaScript — and how to choose SSG, SSR, ISR, or dynamic rendering for SEO.
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.
- Crawl. Googlebot requests the URL and gets back the initial HTML — exactly what
curl https://example.comwould 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. - 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).
- 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/sessionStoragepersistence 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.
| Strategy | When HTML is built | Crawler sees content immediately? | SEO friendliness | Best for |
|---|---|---|---|---|
| CSR (client-side render) | In the browser, after JS runs | No — empty shell first | ⚠️ Risky | Logged-in dashboards, app-like UIs behind auth |
| SSR (server-side render) | Per request, on the server | Yes | ✅ Strong | Personalized or frequently changing public pages |
| SSG (static site generation) | At build time | Yes | ✅✅ Strongest | Docs, blogs, marketing, anything stable |
| ISR (incremental static regen) | At build, then regenerated on a schedule/on-demand | Yes (serves last good static) | ✅✅ Strong | Large catalogs that change but not per-request |
| Dynamic rendering | Per request, bot gets pre-rendered HTML, users get CSR | Yes (for bots) | ⚠️ Workaround | Legacy SPAs you cannot rewrite |
A few notes that the table can’t carry:
- CSR is the default for a plain
create-react-appor 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
.htmlfile 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>, notonClickhandlers that callrouter.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-referencingcanonical, 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
asyncanalytics 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/pricingorsite: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:(orcurl) 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-referencingcanonicalon 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.