Technical SEO for React and Vite SPAs Googlebot actually rewards
Googlebot runs JavaScript. That sentence has cost more teams their rankings than almost any other in SEO, because "can render" and "reliably indexes" are two very different promises — and the gap between them is exactly where a React single-page app leaks traffic.
If you ship a client-rendered Vite SPA, your homepage is, to a first approximation, an empty <div id="root"> and a script tag. A human browser fills it in instantly. A crawler has to do more work — and whether it bothers, and how long it waits, is the whole game. Let's be precise about what actually happens to a React SPA in Google's pipeline, and which fixes move the needle versus which are cargo-cult.
The real problem: two waves of indexing
Google doesn't index a JavaScript page in one pass. It does it in two, and Martin Splitt from Google's Search Relations team has been explicit about why you shouldn't lean on the second one: "Even though Googlebot can render JavaScript, we don't want to rely on that."
Here's the pipeline. Wave one is immediate: Googlebot fetches your URL and parses the raw HTML that came back over the wire — before a single line of your application JavaScript runs. Wave two is rendering: the page goes into a render queue, a headless Chromium eventually executes your JS, and the resulting DOM is parsed for content and links. That second wave is queued and can land anywhere from a few hours to a few weeks after the first.
The consequence is brutal and quiet: for a pure client-rendered app, wave one sees no copy worth indexing and, worse, no links to follow — so the crawler can't even discover your other pages until rendering catches up. You don't get an error in Search Console. You just get slow, partial, occasionally-stale indexing, and you blame the algorithm.
The fix that actually matters: get content into wave one
Every other tactic in this post is a rounding error next to this one. Put real HTML in the initial response so wave one has something to index. Google's own recommendation is server-side rendering, static rendering (SSG/prerendering), or hydration — in that order of preference over client-only rendering.
One option you can cross off: dynamic rendering (sniffing the user-agent and serving a pre-baked page to bots while users get the SPA). Google now explicitly calls it "a workaround and not a long-term solution" and no longer recommends it. It's brittle, it's a second code path that drifts out of sync, and it's a maintenance tax. Don't start there in 2026.
For a Vite + React app the pragmatic, low-cost answer is build-time prerendering: after your normal client build, render each public route's React tree to static HTML and bake the correct <head> (title, description, canonical, Open Graph) into the file Vite ships. The SPA still boots and hydrates for real users — they get the app — but the crawler gets a fully-formed document on the very first byte. Authenticated, interactive routes (a dashboard behind login) don't need any of this; search engines never see them, so leave them client-rendered.
This is exactly how this site is built. Public marketing and blog routes are prerendered to static HTML at build time with route-correct meta tags; the app itself hydrates on top. The dashboard stays a pure SPA. You get crawlable content where it matters and zero SSR runtime to operate.
The SPA-specific mistakes that silently tank you
Even with content in the initial HTML, single-page apps have a handful of failure modes that don't exist in a traditional multi-page site. These are the ones worth auditing:
| Mistake | Why it kills rankings | The fix |
|---|---|---|
Hash routing (/#/pricing) | Everything after # is invisible to crawlers — your whole app indexes as a single URL | Use BrowserRouter (real paths) with a server catch-all |
| Soft 404s | Catch-all routing returns HTTP 200 for URLs that don't exist; Google flags it as a soft 404 and wastes crawl budget | Render a real 404 view and return a 404 status |
onClick navigation | Bots discover URLs from <a href>; a div with a click handler is a dead end | Every in-app link is a real anchor with href; intercept the click for speed, keep the href |
| Client-only meta tags | Title/description set only after JS runs miss wave one entirely | Bake per-route meta into the prerendered HTML |
| Content behind interaction | Copy that only appears after a click/scroll/tab isn't in the rendered DOM Google indexes | Render primary content on load, not on interaction |
| Blocked JS/CSS | If robots.txt blocks your bundles, wave two can't render the page at all | Let Googlebot fetch your JS and CSS |
None of these throw an error. That's what makes them dangerous — the app works perfectly for you in Chrome, so the regression hides until you notice an organic plateau three months later.
How to verify, in five minutes
Don't trust that it works. Check the thing Google actually sees:
- View source, not Inspect. "View page source" is the raw HTML — wave one's input. If your content and links aren't there, neither is Google's. (DevTools "Inspect" shows the rendered DOM and will lie to you here.)
- URL Inspection in Search Console. It shows the rendered HTML Googlebot produced and flags render errors. This is the ground truth for what got indexed.
- Check
<head>per route. Every indexable URL should have its own title, description, and canonical in the initial HTML — not the same homepage tags copy-pasted across the SPA.
The part most teams skip: confirm it in the rankings
Shipping prerendering or fixing hash routing is a deploy like any other, and its real test isn't a green check in a build log — it's whether the affected pages actually start getting crawled, indexed, and ranked. That feedback loop is long (the render queue and re-rank lag stack up to days or weeks), which is precisely why technical-SEO fixes get shipped, forgotten, and never credited with the traffic they earned.
Connecting a specific pull request — "switched to BrowserRouter," "added build-time prerendering" — to the ranking movement it eventually caused is exactly what Code Results does: changepoint detection on your Search Console history, matched back to the deploy that moved the number. Fix the rendering, then watch the page you fixed actually climb.
Sources: Google Search Central — JavaScript SEO basics & dynamic rendering, Search Engine Land — Google no longer recommends dynamic rendering, Botify — JavaScript rendering Q&A with Martin Splitt, Backlinko — JavaScript SEO best practices.