SSR vs CSR for SEO and AI search: SSG, ISR and islands compared
SSR vs CSR for SEO and AI crawlers: what HTML each rendering strategy sends, how SSG, ISR, islands and streaming compare, and which one fits your type of site.
By Crawlito · 9 min read
In short
- Server-side rendering (SSR) and static generation (SSG) put the page text in the first HTML response; client-side rendering (CSR) sends an empty shell that only a JavaScript-running crawler can fill.
- Vercel and MERJ found in December 2024 that none of the major AI crawlers they measured, including OpenAI's crawlers, ClaudeBot and PerplexityBot, executed JavaScript.
- Googlebot renders JavaScript with an evergreen Chromium, but Google still calls server-side rendering or pre-rendering a great idea and labels dynamic rendering a deprecated workaround.
- Streaming SSR keeps late content in the same response but inside a hidden element near the end, so text that matters should sit outside Suspense boundaries that resolve at request time.
- For most public pages SSG or cached SSR is the right default, and CSR belongs behind a login where no crawler needs to read it.
On this page
- What each rendering strategy sends to a crawler
- Why CSR is the risky choice for AI search
- Hybrid rendering, islands and ISR: what changes for crawlers
- The streaming caveat in Next.js
- How to check what your rendering strategy delivers
- Which rendering strategy to choose for your site
- Frequently asked questions
- Check what crawlers receive from your pages
For SEO and AI search, server-side rendering (SSR) and static site generation (SSG) beat client-side rendering (CSR) for one reason: they put your words in the HTML the server sends. Google can render JavaScript, but most AI crawlers read only that first response, so a CSR page looks empty to them. The real choice is between SSG, SSR and the hybrids in between, and it depends on how often your content changes.
This guide compares what each strategy actually delivers to a crawler, with the frameworks that use it, the caveats around streaming and islands, and a recommendation for each type of site.
What each rendering strategy sends to a crawler
The difference between strategies is not how the page looks in a browser. It is what sits in the response body before any script runs. That is the only version a non-rendering crawler ever reads.
| Strategy | What the raw HTML contains | Freshness | Server cost | Typical frameworks and settings |
|---|---|---|---|---|
| CSR (single-page app) | An empty shell such as <div id="root"></div> plus script tags; text appears only after JavaScript runs | Live, fetched in the browser | Lowest: static files on a CDN | Vite or Create React App SPAs, React Router with ssr: false, Nuxt with ssr: false, SvelteKit with ssr = false |
| SSR | The full page, rendered for each request | Current at request time | Highest: a render per uncached request | Nuxt (default), SvelteKit (default), React Router v7 framework mode (default; the upgrade path for Remix v2 apps), Next.js routes that read request-time data such as cookies or headers |
| SSG (static generation) | The full page, built once at deploy | As fresh as the last build | Lowest: files on a CDN | Astro (default), Gatsby (default), Next.js output: 'export', SvelteKit prerender = true, Nuxt prerender: true route rules |
| ISR / stale-while-revalidate | The full page from a cache, generated at build or on first request and refreshed in the background or on demand | Can be one revalidation window behind | Low: renders only when the cache expires | Next.js revalidate, Nuxt isr and swr route rules |
| Islands | Static HTML for the page, with small interactive components hydrated on top | Same as the page's mode (usually static) | Low | Astro client:* directives |
| Streaming SSR | The full page in one response, but suspended sections arrive later, hidden, near the end | Current at request time | Same as SSR | Next.js App Router with loading.js or <Suspense> |
The framework defaults are worth knowing because they decide what you get when nobody made a choice. Nuxt uses universal rendering by default, SvelteKit server-renders every page unless you set ssr = false, React Router says server rendering is enabled by default, Astro prerenders pages at build time by default, and Gatsby uses SSG by default. In the Next.js App Router, pages are Server Components by default and are prerendered to HTML on the server.
None of those defaults is CSR. When a site ends up with an empty shell, it is usually because someone turned SSR off, built a plain SPA, or moved the data fetching into the browser.
Why CSR is the risky choice for AI search
A CSR page depends on the visitor running JavaScript. Browsers do. Googlebot does, eventually: Google's JavaScript SEO guide says a headless, evergreen Chromium renders the page once Google's resources allow, and that pages can wait in a render queue for a few seconds or longer. The same guide still says server-side rendering or pre-rendering is a great idea.
AI crawlers are a different story. In The rise of the AI crawler, published in December 2024, Vercel and MERJ found that none of the major AI crawlers they measured rendered JavaScript. That list included OpenAI's crawlers, ClaudeBot, Meta-ExternalAgent, Bytespider and PerplexityBot. ChatGPT's and Claude's crawlers did download JavaScript files (11.50% and 23.84% of their requests), but did not execute them. The same study said Gemini uses Googlebot's infrastructure and AppleBot renders through a browser-based crawler. Apple's own Applebot page is more cautious and says only that Applebot "may render" a site in a browser. Treat the study as a snapshot from that date. OpenAI, Anthropic and Perplexity do not document JavaScript rendering in their crawler documentation, so the safe assumption is that the raw HTML is all their crawlers see. The details are in do AI crawlers run JavaScript?.
The practical consequence: on a CSR site, ChatGPT search, Claude and Perplexity may receive a title, a few meta tags and a "Loading" message. That is the empty HTML problem, and neither structured data nor an llms.txt file fixes it, because the page content is still missing from the HTML.
Hybrid rendering, islands and ISR: what changes for crawlers
Most modern sites mix strategies route by route, and that is fine. What matters is the rendering mode of each public URL.
- ISR and stale-while-revalidate give crawlers full HTML at static-site speed. The trade-off is freshness: the Next.js ISR guide explains that after the revalidation window passes, the next request still gets the cached, stale page while a new one builds in the background. A crawler can therefore read yesterday's price. For time-sensitive facts, revalidate on demand with
revalidatePathorrevalidateTagwhen the data changes rather than relying on a long timer. - Islands keep the page static and hydrate only the interactive parts. In Astro, components without a directive render to plain HTML, and most
client:*directives add hydration on top. The exception isclient:only, which skips server rendering entirely, so anything inside it is invisible to a non-rendering crawler. - Client-only components inside an SSR page behave like small CSR islands. The Next.js SPA guide explains that
next/dynamicwithssr: falsedisables prerendering for a component so it loads only in the browser, and that a component checking for browser APIs inuseEffectprerenders only itsnullor loading state. It also says that when a browser-only component suspends during prerendering, the fallback becomes the initial HTML, so crawlers that do not run JavaScript see the fallback instead of the component. - Per-route switches cut both ways. Nuxt's
routeRulescan set'/admin/**': { ssr: false }for a dashboard, which is sensible, but the same switch on/products/**hides your catalogue.
The streaming caveat in Next.js
Streaming is the subtle one. In the App Router, Next.js streams a single HTTP response in chunks aligned with Suspense boundaries. The static shell, navigation and fallbacks go first. When a boundary resolves, React sends its finished HTML later in the same response, inside a hidden element, with an inline script that moves it into place:
<!-- In the visual position: only the fallback -->
<template id="B:0"></template><p>Loading reviews…</p>
<!-- Near the end of the same response -->
<div hidden id="S:0"><section><h2>Customer reviews</h2>…</section></div>
<script>/* swaps S:0 into B:0 */</script>The text is in the HTML, which is why the loading.js reference says streaming "does not impact SEO". That statement is about Google and similar search crawlers. For an AI crawler that does not run scripts, that content sits out of order inside a hidden element, and how each AI crawler's text extraction treats it is not documented.
Next.js does send a fully rendered, non-streamed document to user agents it classifies as HTML-limited bots. The option is htmlLimitedBots, added in 15.2.0, and its default list covers Bingbot, applebot, Google crawlers with a -Google suffix or Google- prefix, and link-preview bots such as Twitterbot and Slackbot. Main Googlebot is handled separately as a crawler that runs JavaScript. The list does not include GPTBot, OAI-SearchBot, ClaudeBot or PerplexityBot. Setting the option replaces the default list, so extend it instead of overwriting it:
// next.config.ts
import type { NextConfig } from "next";
// Default list copied from html-bots.ts (Next.js canary, at the time of writing).
const DEFAULT_HTML_LIMITED =
/[\w-]-Google|Google-[\w-]|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight/i;
const AI_CRAWLERS =
/GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|Claude-SearchBot|Claude-User|PerplexityBot|Perplexity-User/i;
const config: NextConfig = {
htmlLimitedBots: new RegExp(`${DEFAULT_HTML_LIMITED.source}|${AI_CRAWLERS.source}`, "i"),
};
export default config;Re-check the default list when you upgrade Next.js, because it can change between releases. The simpler fix is structural: keep the text that matters (product name, price, the main article) outside Suspense boundaries that wait on request-time data. More on this in Next.js and AI search.
How to check what your rendering strategy delivers
You do not need to read framework source to know what a crawler gets. Fetch the page the way a crawler does and look for a sentence you know is on it. The full method is in how to see your page as an AI crawler, with curl.
- Pick a sentence from the main content, not from the navigation.
- Fetch the raw HTML with an AI crawler's user agent and count matches:
UA="Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot"
curl -s -A "$UA" https://example.com/pricing | grep -c "Billed annually"- If the count is 0 but the sentence shows in the browser, the text is rendered client-side.
- Check whether the page streams hidden content:
curl -s -A "$UA" https://example.com/pricing | grep -o '<div hidden id="S:[0-9]*"' | wc -l- On Next.js 15.2 or later, compare with a user agent that Next.js treats as HTML-limited. If the Bingbot response has your content in place and the GPTBot one has it in hidden chunks, the
htmlLimitedBotsfix above applies. Bing documents the Chrome part of its user agent asW.X.Y.Z, the current Edge version; the number below is a placeholder and does not affect the match:
BING="Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm) Chrome/120.0.0.0 Safari/537.36"
curl -s -A "$BING" https://example.com/pricing | grep -c '<div hidden id="S:'One more limit applies whichever strategy you use: Googlebot reads the first 2MB of an HTML file, uncompressed. SSR pages that inline a large serialized data payload before the content can push real text past that point.
Which rendering strategy to choose for your site
| Site type | Recommended strategy | Why |
|---|---|---|
| Marketing or brochure site | SSG | Content changes on deploy; full HTML from a CDN at the lowest cost |
| Blog, docs, help centre | SSG, rebuilt or revalidated on publish | Long text that must be in the HTML; changes are infrequent and known |
| E-commerce catalogue | ISR with on-demand revalidation, or cached SSR | Price and stock must be in the HTML and current; keep cart and account client-side |
| News or fast-moving listings | SSR with short CDN caching | Freshness matters more than render cost |
| Large marketplace (millions of URLs) | SSR or ISR generated on first request | Building every page at deploy is impractical |
| Logged-in SaaS app | CSR is fine for the app; SSG for the public site and docs | Crawlers cannot log in, so the app does not need to be readable |
| Existing SPA you cannot rewrite | Prerender public routes at build time | React Router's prerender option works with ssr: false; other stacks can use a build-time prerenderer |
Avoid dynamic rendering, meaning a separate prerendered version served only to bots. Google calls it a deprecated workaround and recommends SSR, static rendering or hydration instead. It also needs a list of bot user agents that you have to keep current. Prerendering and dynamic rendering covers when a stopgap is still reasonable.
Frequently asked questions
Is SSR better than CSR for SEO?
For pages you want found, yes. SSR puts the content in the first response, so Google can read it at crawl time instead of only after rendering, and AI crawlers that do not run JavaScript can read it at all. CSR is fine for pages behind a login.
Is SSG better than SSR for AI search?
Both deliver the same thing to a crawler: complete HTML. SSG is cheaper and faster to serve, while SSR or ISR keeps fast-changing facts such as prices current. Choose by how often the content changes, not by crawler support.
Does Next.js streaming hurt AI crawlers?
It is not documented either way. Streamed content is in the same response, but out of position inside a hidden element, and GPTBot, ClaudeBot and PerplexityBot are not in Next.js's default HTML-limited bot list. Keep key content outside request-time Suspense boundaries, or add those user agents to htmlLimitedBots.
Do Astro islands work for AI crawlers?
Yes, as long as the content is server-rendered. Static Astro components and islands with directives such as client:load ship HTML, but client:only skips server rendering, so its content is missing from the raw HTML.
Will switching to SSR get my site cited in ChatGPT or Perplexity?
No rendering change can promise that. SSR removes a technical barrier so crawlers can read your text; whether an assistant uses or cites a page depends on many factors outside your HTML.
Check what crawlers receive from your pages
Rendering is the easiest thing to get wrong without noticing, because every page looks fine in a browser. Start with the curl test above on your most important templates. To test one URL without a terminal, run the free check, which needs no account; if the site mixes SSR, static and client-only routes, a full audit covers up to 500 pages and returns evidence with prioritized fixes.
Filed under Rendering and content. Topics: rendering, ssr, javascript, nextjs, ai-crawlers