view-source, through the crawler's eye
SEO is not a checklist you bolt on later. It is a property of the bytes your frontend returns. What ships solid gets indexed now. What waits for JavaScript waits for the render queue.
A raw HTTP response shown as view-source. The lines shipped in the first response, a title, a canonical link, JSON-LD, an h1, and the main product copy, are inked solid and labelled index-eligible in wave one. The lines that only appear after JavaScript runs, an empty root div and the script that injects the heading and body, are ghosted and labelled wave two. The thesis: what a crawler can index is a property of the response bytes.
SEO-friendly frontend architecture, from raw bytes to rankable pages
Reframe SEO as a software engineering property of your response bytes. Trace one causal thread from a rendering decision to what Googlebot receives in wave one, what it must render to see in wave two, what becomes index-eligible, and what can rank.
How Googlebot actually fetches, renders, and indexes#
Most SEO advice is a checklist. This guide is a causal chain instead. To build an SEO-friendly frontend architecture, you first need the model of how a crawler turns your response into an index entry. Google is explicit that this happens in two waves. The diagram below is the whole pipeline. Read it once, because every later section is a zoom into one node of it.
Wave one: the raw HTTP response#
Wave one is the fast path. Googlebot fetches your URL and gets back a stream of bytes. Then it parses that HTML immediately and indexes what it finds. So the question that decides your fate is simple. Is the content in those bytes, or not? Open any page and view source, because that literal text is wave one. Whatever a crawler can index without running your JavaScript lives right there.
Wave two: the render queue and the rendered DOM#
Wave two is the slow path, and its cost is time. When your content only appears after JavaScript runs, Google cannot see it in wave one. Instead the page joins a render queue. Google states plainly that a page queues for rendering and "may stay on this queue for a few seconds, but it can take longer," gated on "once Google's resources allow." Because of that delay, JavaScript-dependent content is indexed later than static content, and new or changed pages surface more slowly.
The one question that decides everything#
Here is the lens the rest of the guide uses. For every element on a page, ask one thing. Is it first-wave, second-wave, or never-seen? First-wave content ships in the response and is index-eligible now. Second-wave content appears only after render, so it waits. Never-seen content sits behind a click, a tab, or a login, and a crawler never triggers it. Consequently this single question turns fuzzy SEO folklore into a property you can inspect in view-source.
The crawler's-eye ledger: toggle a mode, watch index-eligibility change#
Reading about two waves is one thing. Watching them is another. The explorer below makes the lens physical. Flip the rendering mode across CSR, SSR, SSG, and ISR. Then watch two panels update together: the raw HTTP response a crawler receives in wave one, and the rendered DOM it sees only in wave two. Meanwhile the ledger tags every element first-wave, second-wave, or never-seen, and the index-eligibility score recomputes live.
- render-queue wait
- none
- content freshness
- at build time
- crawl-budget cost
- lowest
Wave 1raw HTTP response (view-source)
<!doctype html> <title>Trail runner, size 10 | Acme</title> <link rel="canonical" href="/p/acme-10"> <script type="application/ld+json">Product</script> <h1>Acme trail runner</h1> <main> ...full product copy... </main>
Wave 2rendered DOM (after JS runs)
<h1>Acme trail runner</h1>
<main> ...full product copy... </main>
<nav> ...primary links... </nav>
<!-- the JS-only tab panel is still
empty: nothing clicked it --><title>indexedindexed<h1>indexedindexed<main>indexedindexed<a href>indexedindexedrel=canonicalindexedindexedld+jsonindexedindexed<img src>indexedindexedno SSR fallbacknever seennever seenSSGThe HTML is in the first response, so 7 of 8 elements are index-eligible in wave one, with no render wait. Freshness is at build time. Only the JS-only tab stays unseen, because a crawler does not click to reveal it.
| Page element | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| Title and meta description | first-wave | first-wave | first-wave | first-wave |
| Main heading | second-wave | first-wave | first-wave | first-wave |
| Main content body | second-wave | first-wave | first-wave | first-wave |
| Primary navigation links | second-wave | first-wave | first-wave | first-wave |
| Canonical URL | second-wave | first-wave | first-wave | first-wave |
| Structured data | second-wave | first-wave | first-wave | first-wave |
| Hero image source | second-wave | first-wave | first-wave | first-wave |
| Content behind a JS-only tab | never-seen | never-seen | never-seen | never-seen |
| Index-eligible in wave one | 1 / 8 | 7 / 8 | 7 / 8 | 7 / 8 |
Notice the pattern as you toggle. In CSR the shell ships almost nothing, so the score collapses. In SSR, SSG, and ISR the HTML is in the first response, so the score jumps to near-full. Meanwhile the JS-only tab stays never-seen in every mode. That row is the reminder that some content is invisible to search no matter how you render.
Rendering strategy is an indexability decision, not a performance one#
Teams usually pick a rendering mode for speed. For an SEO-friendly frontend architecture, judge it first by what a crawler can see. Performance still matters, and we will get to crawl budget. First, though, separate the two questions cleanly. One question is what Google can index. The other is how fast the page loads for a human. They are related, yet they are not the same axis.
CSR: the near-empty shell#
Client-side rendering ships a shell and builds the page in the browser. So view-source shows almost nothing but an empty container and a script tag. The primary content does not exist until JavaScript runs, which means it is second-wave by construction.
<!doctype html>
<html lang="en">
<head>
<title>Acme</title>
</head>
<body>
<div id="root"></div>
<script src="/assets/app.js"></script>
</body>
</html> For a public marketing page or a product listing, that is a real risk. The heading, the copy, and the structured data all wait for the render queue. Although Google can render them eventually, "eventually" is exactly the tax you are choosing to pay.
SSR and streaming SSR: first-wave HTML in 2026#
Server-side rendering sends full HTML in the first response, then hydrates in the browser. So your content is first-wave, and hydration is a progressive enhancement on top. In 2026 the picture is richer than the old SSR-versus-CSR split. Streaming SSR flushes HTML as it is ready. React Server Components render on the server and send no component JavaScript at all. Islands and partial hydration ship interactivity only where a page needs it. The common thread is that primary content lands in wave one. For the Cloudflare-specific tradeoffs, our guide on choosing SSR over CSR on Cloudflare walks the same modes at the edge.
SSG: full HTML in wave one#
Static site generation renders the HTML at build time and serves the same file to everyone. Therefore the response is complete on the first byte, with no per-request work and no render wait. This is the baseline an SEO-friendly frontend architecture should default to for content that does not change per request.
<!doctype html>
<html lang="en">
<head>
<title>Trail runner, size 10 | Acme</title>
<link rel="canonical" href="https://acme.example/p/acme-10">
<script type="application/ld+json">{ "@type": "Product" }</script>
</head>
<body>
<h1>Acme trail runner</h1>
<main>...the full product copy ships right here...</main>
</body>
</html> The heading, the copy, the canonical, and the JSON-LD are all right there in the first-wave HTML. So wave one indexes the whole page, and there is nothing left for wave two to catch up on.
ISR: cached HTML with revalidation#
Incremental static regeneration serves cached HTML and refreshes it on a schedule or on demand. So a crawler still gets first-wave HTML, exactly like SSG. The one caveat is freshness. A page can be slightly stale until it revalidates, which is a fair trade for content that changes hourly rather than per request.
Pick by archetype: the decision table#
There is no single winner. The right mode depends on what the page is. Match your app archetype to a strategy, then read across to its first-wave indexability and its crawl-budget cost.
| App archetype | Recommended strategy | First-wave indexability | Crawl-budget cost | Caveat |
|---|---|---|---|---|
| Marketing site | Recommended strategySSG | First-wave indexabilityFull HTML in wave one | Crawl-budget costLowest | CaveatRebuild to publish changes |
| SPA dashboard (behind login) | Recommended strategyCSR | First-wave indexabilityNot indexed, and that is fine | Crawl-budget costNone (not crawled) | CaveatNever public, so SEO does not apply |
| E-commerce PLP | Recommended strategySSR or ISR | First-wave indexabilityFull HTML in wave one | Crawl-budget costLow | CaveatWatch facet URLs and canonicals |
| Docs | Recommended strategySSG | First-wave indexabilityFull HTML in wave one | Crawl-budget costLowest | CaveatLarge sets still need good internal links |
| Blog | Recommended strategySSG or ISR | First-wave indexabilityFull HTML in wave one | Crawl-budget costLow | CaveatISR if you publish very often |
Crawl budget: when wave-two latency becomes a ranking tax#
On a small site, none of this bites. On a large one, wave-two latency turns into a real cost. Crawl budget is the number of pages Google will fetch and process in a given window. So when every page needs a render pass, you spend that budget slower, and new pages wait longer to appear.
The 10,000-page threshold#
Crawl budget is not a concern for most sites. Google says so directly. It matters mainly for large or frequently changing sites, and Google draws the line concretely.
Worked example: CSR versus static rendering on a 12,000-page catalog#
Consider an illustrative team shipping a 12,000-page catalog, just above Google's medium-site threshold. Two choices sit in front of them. First, a CSR single-page app, where the initial response is a near-empty shell and the content only exists after JavaScript runs. Second, an Astro SSG build, where full HTML ships in the first response and is indexed in wave one with no render wait. The CSR path spends crawl budget on rendering every page, so indexing of new and changed pages lags. The static path does not.
Now layer structured data on top, using Google's own published case figure. Take one product page ranking with 40,000 monthly impressions at a 3.5% click-through rate, which is 1,400 clicks per month. Adding valid JSON-LD with no change in ranking position, and applying Rotten Tomatoes' first-party measured +25% click-through rate from structured data, lifts that to 4.375%, or 1,750 clicks per month.
1,400
CSR shell, no structured data
1,750
Static render + Product JSON-LD
Static HTML gets the page indexed in wave one; the JSON-LD then adds +350 clicks a month per page from richer SERP presentation, with no ranking change. Results vary by infrastructure and configuration; the numbers are illustrative first-party figures, not a guarantee.
| Option | clicks per month, one product page, at a fixed ranking position |
|---|---|
| CSR shell, no structured data | 1,400 |
| Static render + Product JSON-LD | 1,750 |
Across 12,000 pages, even fractional adoption compounds. Meanwhile the human-facing budget still applies. Largest Contentful Paint must stay at or under 2.5 seconds at the 75th percentile to pass Core Web Vitals. For the full speed side of that story, see the full speed and Core Web Vitals playbook.
Semantic HTML is a ranking input, not an accessibility nicety#
Semantic HTML gets filed under accessibility, and it does help there. It also feeds ranking, through a chain few articles draw. Here is the chain. A clear heading and landmark structure helps Google extract the main content, which helps it index passages and segments of the page accurately. So a flat wall of divs makes that extraction harder, while a real outline makes it easy.