JavaScript SEO is the work of making content a browser assembles at runtime available to machines that’ll never run a browser. On a server-rendered site the problem doesn’t exist. On a React, Vue, Angular or Svelte application it’s usually the largest single determinant of how much of your product shows up in search.
JavaScript SEO used to be a question of how fast Google would catch up with your rendering. Now it’s also a question of whether a second class of crawler, the ones feeding AI answers, sees anything at all.
Two-pass rendering and why you can’t lean on the render queue
Googlebot crawls the raw HTML response first. If the page depends on JavaScript, the URL joins a queue for the Web Rendering Service, which runs a recent headless Chromium, produces a rendered DOM, and hands that back for indexing and link discovery. Google’s own description of the wait is deliberately loose. The page may sit in that queue for a few seconds, and it can take longer than that, which is a sentence doing a great deal of work.
Read that carefully, because the reassuring half and the expensive half sit in the same sentence. A short typical wait tells you very little about a site with 400,000 URLs and nothing about the slowest decile. The queue gets prioritized, and the pages sitting in it longest have weak internal linking and low perceived importance, which is precisely the set you were hoping to index.
There’s a compounding effect people miss. If your internal links only exist after render, then discovery of the next layer of the site’s waiting on the second pass too. A deep catalogue built this way doesn’t crawl slowly. It crawls in stages, each waiting on the render queue for the one above it.
Other search engines render inconsistently. Most non-search agents aren’t rendering at all.
CSR, SSR, SSG and ISR in practical terms
CSR. The server returns a shell and the client fetches and builds the rest. Fast to develop, cheap to host, and the worst option available for anything that has to be found by a machine.
SSR. HTML generated per request on the server, through dynamic Server Components in the Next.js App Router (opted out of static caching), getServerSideProps, or Nuxt with ssr: true. Right where content is personalized or changes by the minute, at the cost of server response time and server load.
SSG. HTML generated at build time, through generateStaticParams in Next.js or nuxt generate. The best answer for stable content, and the build itself becomes your constraint once you’ve passed tens of thousands of pages.
ISR. Static pages regenerated on a schedule or on demand after deploy, through Next.js revalidate or Nuxt route rules. This is usually the right default for a large catalogue, because you’re getting static delivery with a freshness window you choose per route.
My position, which isn’t popular in front-end teams who’ve optimized hard for developer experience: for any route that has to be indexed, client-side rendering should have to be justified in writing. Static or incrementally regenerated by default. Server-rendered where freshness or personalization genuinely demands it. Client-side only for logged-in surfaces nobody wants indexed.
Dynamic rendering (serving pre-rendered HTML to bots and the application to users) is no longer a recommendation. Google describes it as a workaround rather than a long-term solution, and points at server-side rendering, static rendering or hydration instead. You can still buy your way out of a rewrite nobody will fund, with one warning. It’s a second codepath and it will often drift unnoticed from the first, within a couple quarters.
How to diff raw HTML against rendered HTML
This is the most useful diagnostic in the discipline and almost nobody runs it systematically. Take the raw response and the rendered DOM for the same URL and compare them.
- Save the raw response. A plain
curl -sgives you exactly what a non-rendering crawler receives, and nothing that isn’t there. - Get the rendered version. Use the URL Inspection live test in Search Console and view the crawled HTML, or crawl the same URLs twice in Screaming Frog once in text mode and once with JavaScript rendering and compare the two exports.
- Compare three numbers. Word count, presence of the H1, and the number of links. A large delta on any of them is the finding, and you won’t need a fourth metric to act on it.
- Check the directives specifically. Work out whether
rel=canonical,hreflangormeta robotsarrive via JavaScript. Directives that only exist after render aren’t reliable, and a client-side canonical contradicting one in the raw HTML resolves in ways you won’t enjoy. - Check that navigation uses real anchors. Google can only crawl a link that’s an element with an href attribute, and it names
routerLink,span hrefandonclickhandlers as things it won’t reliably extract.
Run it against five templates, not five pages. The answer is a property of the template, so testing individual URLs gives you the same answer five times over.
Hydration errors are content loss, not console noise
Hydration’s the step where client-side React or Vue attaches to server-rendered HTML. React’s documentation is blunt about the expectations. It expects the rendered content to be identical to the server-rendered content, and tells you to treat mismatches as bugs. React recovers from some hydration errors, and in the worst case it attaches event handlers to the wrong elements.
The search consequence is that content correctly present in the initial HTML can vanish from the rendered DOM, or flicker into a different state. Googlebot indexes the rendered result. Non-rendering crawlers index the server HTML. You now have two versions of your page in circulation, with no symptom in the browser beyond a console warning nobody reads.
Non-deterministic values in render. Date.now(), Math.random() or locale-dependent formatting evaluated while the component renders.
Browser-only reads on a shared component. Touching window, localStorage or cookies in something that also renders on the server.
Scripts that rewrite the DOM during hydration. Personalization and A/B testing tools are the usual culprits here.
Edge or client-side personalization by geo or cookie that overwrites the generic server-rendered HTML during hydration (or contaminates the CDN cache without proper Vary headers).
Hydration problems also show up as layout shift, which puts them on the Core Web Vitals bill as well as the indexing one. Treat a hydration warning in the console as a defect that’s owed a ticket, not as noise.
The AI crawlers that raise the stakes on JavaScript SEO
GPTBot, OAI-SearchBot, ClaudeBot, and PerplexityBot fetch raw HTML. A 41-day controlled crawl experiment run by Vinicius Stanula of LOCOMOTIVE, and reported by Search Engine Land (1,041 hierarchy pages on a 2,400-page directory, split into 748 hard-coded HTML pages and 293 JavaScript-linked pages), found that only Google’s crawler stack executed the JavaScript and followed the injected links.
GPTBot, ClaudeBot, OAI-SearchBot, ChatGPT-User, and the rest found zero of the JavaScript-linked hierarchy pages. Even inside Google’s stack, GoogleOther did almost all the rendering (reaching 48% of the JS group vs. 66% of the HTML group), while Googlebot, the crawler that actually builds the search index, reached just 2% of the JS pages against 5% of the HTML baseline on the test domain. For non-Google crawlers, whatever sits in your initial response body is the whole of what those systems know about your page.
That moves rendering from a question of efficiency to a question of existence. A client-rendered documentation site or catalogue can hold perfectly respectable Google rankings while being absent from every AI answer in its category, and the team will spend a quarter hunting for the cause in their content. It’s in the response body. The rest of that surface is what my AI SEO work covers.
Rendering isn’t an SEO preference. It’s an architectural decision that’s got an SEO bill attached, and the bill lands about eighteen months after anybody made the decision.
How to start fixing JavaScript SEO
Pick your five highest-value templates and diff raw against rendered for each. Then disable JavaScript in the browser and load them, because what you see is roughly what an AI crawler sees. That takes an afternoon and it usually ends the argument.
Move every indexing directive into the server response, including canonical, hreflang and meta robots. Replace click-handler navigation with real <a> elements so links exist before render rather than after. Then move the templates that must be indexed to SSG or ISR and re-run the diff to confirm the delta closed.
None of that requires a replatform. It needs somebody with the authority to hold a rendering decision to an evidence standard, which is most of what I’m doing on technical SEO engagements.

