The promise of modern JavaScript frameworks is real: faster development, component reuse, rich interactivity. The SEO problem is equally real: when a framework renders content entirely in the browser, Googlebot may crawl an empty shell and index nothing. The content exists — users can see it — but it never appears in search results because it wasn't present in the HTML that Googlebot received.
The fix isn't to abandon JavaScript. It's to understand exactly where the rendering gap is and close it deliberately. That means knowing how Googlebot's two-wave pipeline works, which browser APIs it supports, and which rendering strategy your framework uses by default.
Key Takeaways
- Google crawls HTML immediately, but defers JavaScript rendering to a second wave — content rendered only by client-side JS may not appear in the index for hours to days after crawl
- Googlebot runs a limited version of Chrome; it can execute most modern JavaScript but doesn't support all browser APIs, and it processes resources sequentially, not in parallel
- Server-side rendering (SSR) and static site generation (SSG) eliminate the rendering delay — HTML is delivered fully-formed, Googlebot indexes it on the first crawl wave
- Internal links inside JavaScript (dynamically inserted anchor tags, client-side routing without
<a href>elements) may not be followed by Googlebot — reducing crawl depth - Testing JS rendering: the "View Tested Page" in GSC URL Inspection shows exactly what Googlebot saw after rendering — compare it to what a browser sees to find missing content
How Googlebot Processes JavaScript
Google's crawl pipeline operates in two distinct waves, and understanding the gap between them is the foundation of JavaScript SEO.
Wave 1 — HTML crawl: Googlebot fetches the URL and parses the raw HTML it receives from the server. Content present in this HTML is eligible for indexing immediately. For server-rendered pages — WordPress, static HTML, SSR frameworks — this is all that's needed. Googlebot reads it on the first pass, and the page is indexed within hours of crawl.
Wave 2 — JavaScript rendering: A separate queue processes JavaScript execution using the Web Rendering Service (WRS), a headless Chrome variant maintained by Google. Content that only exists after JavaScript executes — dynamically injected text, API-fetched data, client-side routed pages — is indexed only after this second wave. The delay between Wave 1 and Wave 2 varies from a few hours on high-priority pages to several days or even weeks on lower-priority ones.
The consequence is concrete: if your page's title, body text, internal links, or structured data are injected by client-side JavaScript, they may be absent from Google's index during the entire rendering delay window. For frequently updated content — news, product listings, pricing — this creates a persistent lag that compounds over time.
One more detail that surprises developers: even after Wave 2 rendering, Googlebot may not execute all JavaScript. It has a rendering budget per page. Complex SPAs that load dozens of external scripts may hit this budget before all content is rendered, leaving some sections unindexed.
What Googlebot Can and Can't Execute
Googlebot runs a version of Chrome that typically trails stable by one to two releases. It supports the vast majority of modern JavaScript, including ES6+ syntax, the Fetch API, XMLHttpRequest, DOM manipulation, and the React, Vue, and Angular rendering cycles that power most SPAs.
Where Googlebot diverges from a real browser matters more than what it shares:
No localStorage or sessionStorage: Content that requires a user session, stored preferences, or authenticated state won't render. A paywall or personalisation layer that reads from localStorage will produce an empty page for Googlebot.
No IntersectionObserver for lazy-loading triggers: Googlebot doesn't scroll. Content that loads when a user scrolls past it — lazy-loaded sections, infinite scroll feeds — will not be triggered. Googlebot sees only what renders in the initial viewport equivalent.
No Web Workers: Background scripts running in Web Workers don't execute. If critical content depends on data processed in a worker, Googlebot won't see it.
Sequential resource loading: A real browser fetches scripts, stylesheets, fonts, and API calls in parallel. Googlebot processes them sequentially. This makes complex SPAs significantly slower to render for Googlebot than for users — and increases the chance of hitting the rendering budget before all content loads.
No user interaction: Tabs, accordions, modals, and click-to-expand sections that hide content behind a user action are never triggered by Googlebot. Content inside these elements may not be indexed, or may be given lower weight as "hidden" content.
Rendering Strategies and Their SEO Impact
The rendering strategy your framework uses — or that you explicitly choose — determines which wave Googlebot indexes your content on.
Client-Side Rendering (CSR): The server sends a near-empty HTML shell — typically just a <div id="root"></div> and a bundle of JavaScript. The browser builds the entire page from JS. Googlebot receives the same empty shell on Wave 1 and must wait for Wave 2 to see any content. This is the highest-risk pattern for SEO. Create React App, plain Vue 3 with Vite, and Angular without Universal all default to CSR.
Server-Side Rendering (SSR): Each request generates HTML on the server. The browser — and Googlebot — receive a fully-rendered page. Wave 1 indexing works correctly. Higher server CPU cost, but no indexing delays. Next.js with getServerSideProps, Nuxt 3 with server data fetching, and SvelteKit all support SSR.
Static Site Generation (SSG): HTML is pre-built at deployment time. Pages are served as static files — fully rendered, instantly crawlable. Wave 1 indexing works perfectly with no server-side CPU on each request. Best SEO performance overall. Gatsby, Next.js with getStaticProps, Nuxt with generate, and Astro all support SSG.
Incremental Static Regeneration (ISR): A Next.js pattern that regenerates static pages on a configurable schedule or on-demand. Pages are served as static HTML (good for Wave 1), and stale content is refreshed in the background. Good SEO performance with manageable freshness for content that changes occasionally.
Partial Hydration / Islands Architecture: Astro and newer frameworks send static HTML for most of the page and hydrate only interactive components. Ideal for content sites — the vast majority of content is in the static HTML, with minimal JS rendering needed.
For SEO priority order: SSG > SSR > ISR > CSR. For content-focused pages — blog posts, landing pages, documentation, tool descriptions — SSG is almost always the right choice.
JavaScript SEO Issues to Audit
Content missing from the HTML source: This is the core diagnostic. Use View Source (Ctrl+U or Cmd+U) on any page. If your H1, body text, or key nav links don't appear in the raw HTML — only in the browser's rendered DOM visible in DevTools — they are Wave 2 content. Everything in DevTools but not in View Source is JavaScript-rendered and subject to the indexing delay.
Internal links injected by JavaScript: An <a href="/page/"> present in the static HTML is followed by Googlebot on Wave 1. A link created by JavaScript after the DOM loads — through React's router, a client-side navigation component, or dynamically injected markup — may or may not be followed during Wave 2. For reliable crawl depth, internal links should exist in the static HTML. Check your page source: if the nav links, footer links, or in-content links to related pages aren't there, Googlebot may not find those pages via this URL.
Structured data (JSON-LD) added by JavaScript: JSON-LD injected into the DOM by client-side code is subject to the Wave 2 delay. Google recommends placing JSON-LD in the static HTML. If your framework injects structured data via a client-side component (common in React SPAs), the Schema Markup Tester may show the data correctly in a browser but Google may not have indexed it yet.
Canonical tags added by JavaScript: This is critical. Canonical tags must appear in the static HTML or HTTP headers. A <link rel="canonical"> injected by JavaScript is ignored by Google — Google's documentation explicitly states this. If your framework adds canonical tags via client-side code, they provide no canonicalisation signal. Verify your canonical tag appears in View Source, not only in DevTools.
Title and meta description set by JavaScript: Frameworks like React Helmet and Next.js Head set the page title and meta tags via JavaScript. SSR/SSG frameworks inject these into the static HTML correctly. Pure CSR frameworks may not — the title and description visible in the browser may not be what Google indexes.
Testing JavaScript Rendering
GSC URL Inspection → Test Live URL → View Tested Page: This is the authoritative source. Google shows the rendered HTML after Wave 2 processing, plus a screenshot of the rendered page. Compare the rendered HTML to what you see in View Source — any content present in the rendered HTML but not the source was JavaScript-rendered. Any content present in View Source but not in the rendered HTML indicates a rendering failure.
View Source vs DevTools: The fastest manual check. View Source is what Googlebot receives on Wave 1. DevTools Elements panel is the post-JavaScript DOM. For every key piece of content — headings, body text, links, structured data, canonical — verify it exists in View Source.
Page Speed Inspector: Checks TTFB, render-blocking scripts, and compression. Render-blocking JavaScript delays when content becomes available for both users and Googlebot. A high render-blocking script count increases the chance that Googlebot's rendering budget runs out before all content loads. The inspector also surfaces inline JavaScript byte counts — large inline JS blobs are a signal that significant rendering work is being pushed to the client.
Disable JavaScript in browser: Chrome DevTools → Command Menu → "Disable JavaScript" then reload. This simulates Wave 1 — what Googlebot sees before any JS executes. If your page is blank or nearly empty with JS disabled, you have a CSR-only architecture and Wave 1 indexing will fail.
Practical Recommendations by Framework
| Framework | Default rendering | SEO recommendation |
|---|---|---|
| Next.js (App Router) | SSR by default | Add export const dynamic = 'force-static' for content pages; avoid force-dynamic unless page data changes per-request |
| Next.js (Pages Router) | SSR via getServerSideProps, SSG via getStaticProps |
Use getStaticProps for blog posts, landing pages, and tool docs |
| Nuxt 3 | SSR by default | Use useFetch for server-side data; avoid useAsyncData with server: false for SEO-critical content |
| Vue 3 (Vite) | CSR | Add Nuxt or VuePress for SSR/SSG; do not use plain Vite for content pages |
| React (CRA) | CSR | Migrate to Next.js or Gatsby for any content-driven pages; CRA is unsuitable for SEO |
| Gatsby | SSG | Default is excellent for SEO; ensure gatsby-plugin-react-helmet or Gatsby Head API is used for meta tags |
| Astro | SSG (with optional hydration islands) | Ideal for content sites; use output: 'static' and minimise client:load directives on content-bearing components |
| SvelteKit | SSR by default | Use prerender = true on content pages; avoid ssr: false for SEO content |
Check What Google Actually Sees on Your Pages
Free, no signup. The Page Speed Inspector reveals render-blocking scripts, TTFB, and missing compression that affect how fast Googlebot can process your JS-rendered pages — without a Chrome extension or local setup.
Try the Page Speed Inspector Free →Affiliate disclosure: Some links on this page may be affiliate links. We only mention tools we've personally used and have an honest opinion about. Affiliate revenue helps keep ByteWaveNetwork's tools free and maintained. We are not paid by any of the tools compared in this article for favorable coverage.