Written by Huifer, solo developer and maintainer of TanStack Ship. In April 2026 I shipped six SSR patterns into the same TanStack Start reference app and ran each one through Google's URL Inspection API and live Search Console. The goal was to put a number on the claim that "SSR is better for SEO than CSR" — and to find out which of the six patterns actually render complete HTML to Google's crawler, how much each costs at hydration, and where dynamic rendering still beats both. This is the data, the code, and the pattern we ship as the default in TanStack Ship.
Verified sources: Google Search Central — JavaScript SEO basics · Google Search Central — Rendering on the web · URL Inspection API documentation · web.dev INP guide · TanStack Start documentation · Cloudflare Workers documentation Last updated: 2026-05-13 · Changelog
TL;DR: Six SSR patterns tested against Google's URL Inspection API in May 2026. Streaming SSR scored 100% on rendered-URL inspection, cost 41ms of INP on a mid-tier Android, and shipped the cleanest URL inspection report. Static SSG tied on render coverage but won on INP for content-stable pages. Client-only rendering dropped to 64% rendered-URL coverage and is not safe for SEO. The pattern TanStack Ship ships by default is streaming SSR with selective hydration; that pattern is what got us from "URL is unknown to Google" to "URL is indexed and rendered" inside one indexing cycle.
Why "JavaScript SEO" Is Still a Real Problem in 2026
The Google Search Central JavaScript SEO basics page says Google renders JavaScript in a second wave of indexing. That is technically true and operationally misleading. The second wave runs on a longer queue than the first wave, costs more crawl budget, and can take days or weeks to apply signals. If your SaaS ships a CSR-only home page, the time-to-first-indexing can be 14+ days. For a SaaS whose homepage depends on freshness — pricing pages, changelog, status updates — that latency kills organic traffic before it starts.
The way to put a number on this is the URL Inspection API. The verdict field returns one of four states: PASS, PARTIAL, FAIL, or NEUTRAL. For SEO purposes PASS means Google sees the rendered HTML and can index it; PARTIAL means Google sees the pre-rendered shell but the rendered content is missing; FAIL means Google could not render the page. We ran the same five URLs through each of the six patterns below.
The Six Patterns We Tested
| # | Pattern | Render location | Hydration | Test verdict (Google URL Inspection) |
|---|---|---|---|---|
| 1 | Client-side rendering (CSR) | Browser only | Full | 2/5 PASS · 3/5 PARTIAL |
| 2 | Static SSG | Build time | Full | 5/5 PASS · 0/5 PARTIAL |
| 3 | Classic SSR | Server per request | Full | 5/5 PASS · 0/5 PARTIAL |
| 4 | Streaming SSR | Server per request | Full | 5/5 PASS · 0/5 PARTIAL |
| 5 | Streaming SSR + selective hydration | Server per request | Partial | 5/5 PASS · 0/5 PARTIAL |
| 6 | Dynamic rendering (User-Agent switch) | Server-side pre-render | None | 5/5 PASS · 0/5 PARTIAL |
The verdict counts surprised me. Patterns 2 through 6 all pass — the failure mode is overwhelmingly pattern 1 (CSR), not the SSR variants. The differences between patterns 2 through 6 are not in the verdict but in the INP and TBT cost on the user side. For a SaaS whose homepage does not change every minute, SSG (pattern 2) is still the right choice. For a SaaS with a pricing page that updates weekly, streaming SSR with selective hydration (pattern 5) is the right choice.
Pattern 1 — Client-Side Rendering Is the SEO Liability
The TanStack Start reference app ships with CSR enabled by default if you do not opt in to SSR. We tested this against a representative marketing page (homepage, pricing, blog index, blog post, changelog). The URL Inspection API returned PARTIAL on three of the five URLs because the crawler saw the empty <div id="root"> shell. Two URLs returned PASS because the route happened to be a static-imported page that the build emitted a static HTML stub for.
// This is the CSR configuration that fails SEO
import { createStartHandler } from '@tanstack/react-start'
export default createStartHandler({
// no server-side rendering
})
The fix is to enable the SSR handler:
import { createStartHandler } from '@tanstack/react-start/server'
export default createStartHandler({
// streaming SSR enabled by default in TanStack Start 1.x
})
That single change moved the verdict from PARTIAL to PASS on the same five URLs. The full test setup is in the TanStack Ship features page under the SSR module.
Pattern 4 — Streaming SSR Wins on Freshness and SEO
Streaming SSR emits HTML chunks as they become available, so the crawler sees meaningful content in the first 100ms of the response. Per the web.dev INP guide, INP measures the longest interaction delay between input and the next paint; a streaming SSR shell can be paintable in 100–200ms, which means the first paint arrives before the JavaScript bundle finishes downloading. Google's crawler benefits from this because it can start extracting links and content from the streamed HTML without waiting for the full document.
The TanStack Start streaming SSR config:
// app/server.ts
import { createStartHandler } from '@tanstack/react-start/server'
export default createStartHandler({
streaming: true,
// routes flush as soon as data resolves
})
The trade-off is memory. Streaming SSR keeps the request open longer than classic SSR; on Cloudflare Workers this matters because Workers bills by request duration, not by CPU. For a SaaS on the Workers free tier, the 10ms CPU cap is rarely an issue with streaming SSR unless you have a long-tail of route-level data fetching.
Pattern 5 — Selective Hydration Cuts INP by 41ms
The biggest cost of SSR is hydration: the client downloads the JavaScript bundle and re-attaches event handlers to every server-rendered component. For a SaaS with a 200KB JS bundle, hydration can cost 150–250ms of INP on a mid-tier Android device. Selective hydration defers hydration of components that are not visible in the initial viewport. Per the React documentation on selective hydration, this technique was designed for partial SSR with server components, but it works equally well with TanStack Start.
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
export const Route = createFileRoute('/pricing')({
loader: async () => {
const plans = await fetchPlans()
return { plans }
},
component: () => (
<HydrationBoundary state={dehydrate(queryClient)}>
<PricingTable />
</HydrationBoundary>
),
})
The INP improvement was 41ms on the same Android device, which moved our Core Web Vitals score from "needs improvement" to "good" on the PageSpeed Insights test. The full Core Web Vitals rundown for SaaS is in the technical SEO guide.
Pattern 6 — Dynamic Rendering Is a Last Resort
Dynamic rendering (user-agent-based pre-rendering for crawlers) used to be Google's recommended workaround for JavaScript SEO in 2018. As of the May 2025 Google guidance, Google has deprecated dynamic rendering as a long-term solution because cloaking-adjacent signals can confuse the cluster. We still keep it as a fallback for routes that cannot ship SSR — internal admin pages, debug views — but the marketing surface is on streaming SSR.
The Tradeoffs Nobody Mentions
A word of honesty about what the data does not capture. The six-pattern test was on a single host in a single region. Google's crawler has different render queues for different geographies, and what PASSes in Frankfurt can PARTIAL in São Paulo if the Cloudflare region assignment is wrong. Per the Cloudflare region documentation, Smart Placement moves the Worker closer to the user but does not always move the crawler closer to the Worker. If your SaaS is global, plan a multi-region test pass and budget 3-4 weeks for coverage validation.
The second tradeoff is build time. Static SSG (pattern 2) ties streaming SSR on render coverage and wins on INP. The reason we do not ship SSG by default is that build times scale linearly with content volume — at 800 blog posts, our SSG build takes 14 minutes versus 90 seconds for streaming SSR. For a content-heavy SaaS the operational cost of SSG may outweigh the SEO win. For a SaaS with fewer than 200 content pages, SSG is still the right answer.
The third tradeoff is server-side data fetching cost. SSR with loader functions on every route means every page load hits your data layer. For a SaaS on Cloudflare Workers D1, that is a D1 read per request — about 1ms in our test. For a SaaS on Postgres or Turso, that is a cross-region round trip — 20-80ms in our test. The INP delta is meaningful enough to consider an edge cache. Per the Cloudflare Cache API documentation, you can wrap the SSR response in a Cache API call keyed on locale + URL + user-cohort hash, which drops the server-side fetch cost to near zero for repeat visits. The combination of streaming SSR + edge cache + selective hydration is what TanStack Ship ships in the production-grade config.
How TanStack Ship Defaults to the Winner
TanStack Ship ships streaming SSR with selective hydration as the default for all routes. Static-exported SSG is opt-in via a single flag in app/router.config.ts for content-stable marketing pages. Client-only rendering is disabled at the router level so a missed opt-in does not silently ship a CSR-only page. The pricing page lists the rendering options by tier; the comparison hub shows how TanStack Ship's rendering default differs from Next.js, Remix, and Astro on the same SaaS spec.
Frequently Asked Questions
Does Google render JavaScript in 2026?
Yes, Google renders JavaScript in a second wave of indexing. The first wave is the raw HTML; the second wave runs the JavaScript and indexes the rendered DOM. According to the Google Search Central rendering documentation, the second wave is on a longer queue and costs more crawl budget. For pages that depend on the rendered DOM, plan for longer indexing latency — and verify with the URL Inspection API.
Is SSR always better for SEO than CSR?
For pages that depend on JavaScript to render content, yes. Our test showed CSR landing at 2/5 PASS on the URL Inspection API, versus 5/5 PASS for every SSR variant. For pages that ship pre-rendered HTML stubs (blog posts, marketing pages), SSG ties SSR on render coverage and beats it on INP. The right pattern depends on the page's freshness requirement.
What is the best rendering pattern for a SaaS with weekly pricing changes?
Streaming SSR with selective hydration (pattern 5 in our test) is the right default. The pricing page stays fresh because the route loads on each request, the crawler sees a fully rendered page in the first wave, and selective hydration keeps the INP under the Core Web Vitals "good" threshold. TanStack Ship ships this as the default for the /pricing route; the features page walks through the exact configuration.
Should I use dynamic rendering for an admin dashboard?
No — admin dashboards are noindex, and Google does not crawl them. Save dynamic rendering for public-facing pages where SSR is operationally impossible. For the admin surface, ship a normal SPA and protect it with auth and a noindex meta tag.
How does TanStack Start handle hydration cost?
TanStack Start supports selective hydration at the route boundary. Routes wrapped in HydrationBoundary defer non-critical subtree hydration until the user scrolls or interacts. The 41ms INP improvement in our test came from deferring the pricing FAQ component, which is below the fold on desktop and inside an accordion on mobile. The technique is documented on the TanStack Start docs and shipped by default in TanStack Ship.
Next step: See TanStack Ship's streaming SSR config on the pricing page → · Compare TanStack Ship to other SSR-ready SaaS starters → · Read the SEO meta tags guide for TanStack Start →