React SEO

React renders client-side by default, so crawlers see an empty shell until JavaScript runs. Here's how Google actually processes React apps, which rendering strategy to pick, and how to fix routing, metadata, and the render-timeout duplicate-content trap.

First published: Jun 26, 2026 · Last updated: Jul 18, 2026 · Advanced
demand #1 in JavaScript Frameworks#11 in Platform SEO#76 in Technical SEO#102 on the site

React is not bad for SEO — but client-side rendering by default is. Out of the box (CRA, Vite + React), the server ships an empty shell and the browser builds the page, so crawlers see nothing until JavaScript runs. Google can render React via its Web Rendering Service, but rendering is queued, delayed, and can time out — Gary Illyes has shown render timeouts leaving boilerplate-only pages that get marked as duplicates. AI-crawler rendering contracts vary by provider, so CSR-only content adds a coverage risk. The fix is rendering strategy: SSR or SSG (most easily Next.js or Remix) puts content in the initial HTML, hydrated with hydrateRoot (not createRoot) so server and client output match exactly. Then use History API routing, real <a href> links, correct status codes, and version-appropriate metadata: React 19 hoists <title>/<meta>/<link> natively, otherwise react-helmet-async (never the unmaintained original react-helmet).

TL;DR — React’s SEO problem isn’t React — it’s client-side renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. by default. CRA and Vite + React ship an empty shell and build the DOM in the browser, so the raw HTML a crawlerA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. fetches has no content. Google can render it via the Web RenderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. Service (evergreen Chromium), but rendering is queued separately, can be delayed, and can time out — Gary Illyes has documented render timeouts leaving boilerplate-only pages that then get flagged as duplicates. Bing renders JS less reliably; AI-crawlerA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. rendering varies by provider. The fix is rendering strategy: SSR or SSG (most easily Next.js or Remix) puts content in the initial HTML — and if you’re hydrating server-rendered markup, use hydrateRoot (not createRoot) and treat any server/client mismatch as a bug, not a warning to suppress. Then use History API routing (not hash URLs) and real <a href> links. For <head> metadata: React 19 hoists <title>/<meta>/<link> natively; on React 18 or for advanced needs, use react-helmet-async (never the unmaintained original react-helmet). Set correct HTTP status codes. There’s no ranking bonus for SSR — it just makes content reliably indexable. For the general rendering mechanics, see the JavaScript SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. and headless CMSA content management system (CMS) is software that lets users create, manage, and publish digital content — like blog posts and pages — without writing raw code. WordPress, Drupal, and Joomla are the most common open-source CMS platforms. topics.

What actually makes React hard for SEO

React is a component-based JavaScript library, and out of the box — Create React App, Vite + React — it runs client-side. The server returns a near-empty document (famously just a <div id="root"></div>) plus a bundle of JavaScript, and the browser executes that JavaScript to construct the DOM. Contrast that with a server-rendered page (WordPress, a Rails app), where the full HTML — content, headings, links — arrives in the very first response.

So the question that decides everything is: what’s in the raw HTML before any JavaScript runs? For a default React app, the answer is “almost nothing.” Right-click → View Source on a CRA app and you’ll see the shell, not the content. That’s exactly what a crawler gets on its first fetch.

To be precise about where the responsibility actually sits: React the library isn’t CSR-only. React DOM ships client rendering (createRoot), server rendering (streaming and static APIs), and hydrationActivating server-rendered HTML in the browser by attaching JavaScript handlers. APIs — the library supports all of it. The empty-shell problem is a property of the default toolchain (Create React App, Vite + React with no server), which wires up only the client APIs and nothing that renders to HTML on the server. Swap the toolchain — Next.js, Remix, or React’s own server-rendering APIs — and the same library ships full HTML on the first response.

This is the React-specific application of the broader JavaScript SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript. problem — go there for the general failure modes (parity, interaction, state, timing). Here I’ll focus on what’s specific to React and how to fix it.

How Google actually processes a React app

Google handles JavaScript in three phases: crawl → render → indexStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed.. GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. fetches the URL, the rendered DOM is built later by the Web Rendering Service (WRS) — an evergreen version of Chromium, the same engine as Chrome — and then the rendered output gets indexed and its links extracted. Evidence for this claim Google processes JavaScript pages through crawling, rendering, and indexing using its Web Rendering Service. Scope: Google Search rendering behavior. Confidence: high · Verified: Google: JavaScript SEO basics

The important nuance is when rendering happens. Rendering is resource-intensive, so it’s queued separately from the initial crawl. Martin Splitt described the flow plainly: “we do an HTTP request, and we get something back … some barebone HTML and all it does is load the JavaScript and run the JavaScript. Then, this HTML … goes into rendering. Rendering runs JavaScript — boom!, a lot of content happens that wasn’t there before.” For a CSR React page, the “boom” is your entire page — none of it exists until that render step runs.

A caveat worth carrying: don’t over-index on the old “two waves of indexingStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed.” model. Splitt himself walked it back, calling the wave “an oversimplification.” The practical takeaway isn’t “there’s a formal Wave 2 with defined timing” — it’s that rendering is a distinct, deferrable, fallible step, and CSR puts 100% of your content on the wrong side of it.

Two more facts about the renderer that bite React apps specifically:

  • It’s stateless. GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. does not retain localStorage, sessionStorage, or cookies between page loads. Any content or routing that depends on client-side state is invisible to the crawler.
  • It can give up. The renderer enforces a timeout. If your main content loads slowly — large bundles, waterfalls of API calls — rendering can finish before your content arrives, and Google indexes the incomplete page.

The render-timeout trap (and why it creates duplicates)

This failure mode is rarely explained well, and it’s the most damaging one for React apps. Gary Illyes described it directly: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out … and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.”

Walk through what that means for a CSR React app. Your header, nav, and footer are boilerplate that loads fast. Your actual page content — the part that makes each URL unique — is fetched and rendered by JavaScript, and it loads slowly. Rendering times out. Google is left with header + nav + footer on every URL. Now every page looks identical, and Google marks them as duplicates of each other in Search ConsoleA free Google service that reports how a site performs in Google Search and surfaces problems with how Google crawls, indexes, and serves it. It's first-party data straight from Google — but you don't need it to appear in results..

Illyes’s own fix is the actionable bit: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first and see if that helps.” But the more durable answer is to not depend on the render step for your main content at all — which means SSR or SSG.

Rendering strategies for React

This is the single most impactful decision. The options, roughly worst to best for SEO:

  • CSR (default React). Server sends the shell; browser builds everything. Content is delayed by the render queue and exposed to the timeout. Worst for SEO. Fine for authenticated dashboards you don’t want indexed anyway. Evidence for this claim Client-only React rendering constructs UI in the browser; server rendering APIs produce HTML before browser hydration. Scope: React rendering mechanics; SEO impact depends on what the initial response contains. Confidence: high · Verified: React: hydrateRoot React: Server APIs
  • Pre-rendering. Build-time rendering without a full SSR framework — tools like react-snap or a prerenderThe Speculation Rules API is a Chromium browser API (Chrome/Edge 109+) that lets a site tell the browser which same-site pages to prefetch (download the HTML document) or prerender (fully load and render in an invisible tab) before a visitor clicks — so the next navigation can be near-instant. It's a browser-side performance feature for real users, not a crawling, indexing, or ranking signal. service crawl your app and save static HTML. Lighter-weight; works for simpler, mostly-static sites.
  • SSG (Static Site Generation). HTML built once at deploy time and served as static files. Fastest, content always present in the raw HTML. Limited for highly dynamic or per-user content; large sites get slow builds.
  • SSR (Server-Side Rendering). The server executes React per request and sends full HTML. Content immediately available to crawlers; always fresh. Costs a Node.js server and slightly higher TTFBTime to First Byte — the time from the start of a request to when the first byte of the response arrives. It's a diagnostic metric (not a Core Web Vital) and a major input to FCP and LCP; ≤0.8 s is good..
  • Hybrid / ISR (Incremental Static Regeneration). A Next.js feature that regenerates static pages in the background — static speed with periodic freshness.
StrategyContent in initial HTML?SEO riskBest for
CSR (raw React)NoHighestLogged-in dashboards, not-indexed apps
Pre-renderingYes (build time)LowSmall, mostly-static sites
SSGYes (build time)LowestBlogs, docs, marketing
SSRYes (per request)LowFresh, dynamic content
ISR / hybridYesLowContent that changes hourly/daily

And one strategy to skip on new builds: dynamic rendering — detecting the crawler user-agent and serving it a pre-rendered version while users get CSR. Google now calls it “a workaround and not a long-term solution” that “creates additional complexities and resource requirements,” and recommends server-side rendering, static rendering, or hydration instead. (Bing recommended dynamic rendering back in 2018, but that guidance is dated — since 2019 BingbotBingbot is Microsoft Bing's primary web crawler — the bot that discovers, fetches, and renders pages to build the Bing index. That index also powers Yahoo, DuckDuckGo, Ecosia, and Microsoft Copilot, so Bingbot's reach is far wider than Bing's own search-market share. renders via Microsoft Edge / Chromium, and SSR/SSG is the right call there too.)

There’s a myth worth killing here: SSR is not a ranking boost. As John Mueller put it, “there are no SEO ranking bonuses for implementing it one way or another” — the different rendering methods are “just different ways of making the content indexable.” SSR’s value is reliable indexability (and often better Core Web VitalsGoogle's three real-user UX metrics — LCP (loading), INP (responsiveness), and CLS (visual stability) — used by Google's ranking systems, with no official weight attached, measured on field data. from a faster First Contentful Paint), not a magic ranking lever.

Hydration must match exactly — that’s a bug boundary, not an SEO technique

SSR and SSG both hand the browser HTML that already has your content in it. React then has to attach to that markup on the client, and that’s a different API from a plain client render:

  • createRoot renders React into a DOM node from scratch — no existing markup expected. Use it for CSR-only apps.
  • hydrateRoot attaches React to HTML that react-dom/server already generated, and expects the client’s first render to produce output identical to what the server sent. If you’re SSR/SSG, you want hydrateRoot, not createRoot — calling createRoot on server-rendered markup means React discards it and re-renders from scratch, throwing away the exact SEO benefit you set up SSR/SSG to get.

Mismatches between server and client output are a real risk in React apps doing SEO fixes — a Date.now() in a title, a locale-dependent format, an if (typeof window !== 'undefined') branch. React’s own docs are blunt about what happens then: it warns about mismatches in development, but “there are no guarantees that attribute differences will be patched up in case of mismatches.” The guidance is to treat mismatches as bugs and fix them — not to suppress the warning and assume content parity. For SEO specifically: don’t assume your rendered content and metadata match what the server sent just because the page looks right in the browser. Diff the server HTML against the post-hydration DOM directly (the View Source vs. Inspect Element check from the testing section below is the fast version of this) rather than trusting a clean console.

React Router and URL structure

React Router handles navigation in the browser without server round-trips, which is fine for SEO if configured correctly:

  • Use the History API, not hash routing. BrowserRouter uses pushState and produces clean, crawlable URLs (/products). HashRouter produces /#/products, and Google cannot reliably resolve hash-based URLs — the old AJAX-crawlingCrawling is how search engines use automated bots (like Googlebot and Bingbot) to discover URLs and download pages. A page has to be crawlable to be indexed, but crawling on its own isn't a ranking factor. scheme that made them work is deprecated. Use the History API.
  • The server must handle those URLs too. With History API routing, every “page” needs a real URL the server can respond to — critical for SSR, and necessary so a direct hit or refresh on /products doesn’t 404.
  • <Link> renders a real anchor. React Router’s <Link> component outputs an <a href>, which is crawlable. Navigation built on onClick handlers without an anchor is not crawlable — Google only follows real <a href> links.

Managing metadata: react-helmet, react-helmet-async, and React 19’s native tags

Through React 18, React never updated the document <head> on route changes natively — every route’s <title>, meta descriptionThe meta description is an HTML head tag — `<meta name=\"description\" content=\"…\">` — that suggests a short summary of the page for the search snippet. It's not a Google ranking factor, and Google rewrites it the majority of the time, but a good one can still lift click-through., canonical, and Open GraphOpen Graph (OG) tags are `<meta>` elements in a page's head, defined by the Open Graph protocol (ogp.me, created by Facebook), that describe a page as a shareable object — its title, description, image, URL, and type. They control how a link preview card looks when the page is shared on Facebook, LinkedIn, Slack, Discord, WhatsApp, and iMessage. They are not a direct Google ranking factor, though Google reads og:title, og:image, and og:site_name as inputs to how a result appears. / Twitter tags had to be set by a library. React 19 changed that: components can render <title>, <meta>, and <link> tags directly, and React hoists them to <head> on its own — working with client-only apps, streaming SSR, and Server Components alike. React 19.2 is the current stable release as of mid-2026, so this now applies to any app on a current React version.

That means the right answer depends on your React version and what you actually need:

  • React 19, standalone app, only basic tags. Render <title>/<meta>/<link> in your components directly — no library needed.
  • React 19, but you need htmlAttributes/bodyAttributes, SSR context serialization, onChangeClientState, prioritizeSeoTags, or titleTemplate. Native hoisting doesn’t cover these — use react-helmet-async. Its own docs call this out directly: without those specific needs, you may not need the package at all on React 19.
  • React 18 or earlier, standalone app. Native hoisting doesn’t exist yet — use react-helmet-async. It’s actively maintained (major version 3, and it detects your React version at runtime) and supports SSR.
  • The original react-helmet. Don’t use it, on any React version. It’s unmaintained — no release since 2020 — and has known bugs under React 18’s concurrent rendering.
  • Next.js apps. Use Next’s own Metadata API (the metadata export / generateMetadata in the App Router) regardless of React version — don’t bolt on Helmet, and don’t rely on native React tag hoisting either. The framework owns the document in a Next.js app.

One reliability rule carries over from JavaScript SEO generally, and applies no matter which of the above you use: HTML-level metadata beats JS-injected metadata. A canonical tag injected by client-side JavaScript is far less reliable than one present in the server-rendered HTML — which is another argument for SSR/SSG.

AI crawlers make this urgent

The 2026 wrinkle: rendering behavior for GPTBot (OpenAI), ClaudeBot (Anthropic), PerplexityBot, and other agents is provider- and version-specific. A CSR React app ships every crawler the same empty shell the first Googlebot fetch sees, and current provider documentation does not establish one shared render step that will fill it in. As generative engines become a bigger discovery surface, SSR/SSG stops being a Google-only concern: raw HTML maximizes coverage without assuming a universal crawler limitation. (The headless CMSA content management system that separates the content repository from the presentation layer, delivering content via API to any front-end framework rather than rendering HTML server-side itself. It doesn't specify the rendering mode, hosting, cache, preview security, or publishing workflow — those are separate decisions. topic covers this AI-crawler reality in more depth.)

Testing what Google actually sees

Don’t trust your browser — your DevTools Inspector shows the rendered DOM (post-JavaScript), which is exactly what a crawler-without-JS does not see. Use the right tools:

  • View Source vs. Inspect Element. View Source is the raw HTML (what crawlers get before JS). Inspect Element is the rendered DOM. If content is in Inspect but missing from View Source, it’s JavaScript-dependent.
  • URL Inspection ToolA Google Search Console feature that reports how Google sees one specific URL on a property you own. By default it shows the last-indexed snapshot; a separate \"Test live URL\" mode fetches the current version. (Search ConsoleGoogle's free tool for monitoring crawling, indexing, and search performance.) — the most authoritative check. Run a live test and look at the rendered HTML, the screenshot, and the page resources / console messages to see what Google actually rendered and what failed to load.
  • Rich ResultsRich results (formerly 'rich snippets') are enhanced search listings — stars, images, prices, breadcrumbs, video thumbnails, and more — that Google and Bing build from structured data. They're a display feature, not a ranking factor, and eligibility never guarantees they'll show. Test — a quick rendered-HTML check without verifying the site.
  • Disable JavaScript in DevTools and reload — a fast simulation of a crawler that doesn’t execute JS (and a decent proxy for what AI crawlersAI crawlers are bots from AI companies that fetch web pages to train language models, build AI-search indexes, or answer live user questions. They come in three categories, each with its own user-agent tokens and its own robots.txt controls. see).
  • Search Console Coverage report — “Discovered, currently not indexed” can signal a render queue backlog; clusters of duplicate pages can signal the render-timeout boilerplate trap.
  • JS-rendering crawlers — Ahrefs Site Audit and Screaming Frog (JS-rendering mode) render pages at scale so you can diff raw vs. rendered across the whole site.

Next.js and Remix (the practical answer)

If SEO matters and you’re on raw CSR React, migrating to a framework that renders on the server is usually the right move. Next.js is purpose-built for this — SSR and SSG out of the box, ISR, the App Router, a built-in Metadata API, automatic code splitting, and image optimization. Remix is the web-standards alternative, built on fetch/Request/Response with SSR by default and a strong progressive-enhancement story. Next.js gets its own deep dive — I’m keeping it brief here on purpose. The point for React SEOReact SEO is the practice of making React apps crawlable and indexable. React renders client-side by default, so the raw HTML is near-empty until JavaScript runs — SSR or SSG puts the content back in the initial response where crawlers (and AI bots) can reliably see it. is narrower: the framework exists to move your content out of the browser-only render step and into the initial HTML.

React is good for SEO when you treat rendering as an architecture decision rather than an afterthought. Pick SSR or SSG for anything that needs to rank, keep your links and routing honest, manage metadata per route, and let Google’s own tools — not your browser — tell you what actually rendered.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.