Skip to content

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

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.

StrategyWhat the raw HTML containsFreshnessServer costTypical frameworks and settings
CSR (single-page app)An empty shell such as <div id="root"></div> plus script tags; text appears only after JavaScript runsLive, fetched in the browserLowest: static files on a CDNVite or Create React App SPAs, React Router with ssr: false, Nuxt with ssr: false, SvelteKit with ssr = false
SSRThe full page, rendered for each requestCurrent at request timeHighest: a render per uncached requestNuxt (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 deployAs fresh as the last buildLowest: files on a CDNAstro (default), Gatsby (default), Next.js output: 'export', SvelteKit prerender = true, Nuxt prerender: true route rules
ISR / stale-while-revalidateThe full page from a cache, generated at build or on first request and refreshed in the background or on demandCan be one revalidation window behindLow: renders only when the cache expiresNext.js revalidate, Nuxt isr and swr route rules
IslandsStatic HTML for the page, with small interactive components hydrated on topSame as the page's mode (usually static)LowAstro client:* directives
Streaming SSRThe full page in one response, but suspended sections arrive later, hidden, near the endCurrent at request timeSame as SSRNext.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.

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 revalidatePath or revalidateTag when 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 is client: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/dynamic with ssr: false disables prerendering for a component so it loads only in the browser, and that a component checking for browser APIs in useEffect prerenders only its null or 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 routeRules can 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:

html
<!-- 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:

ts
// 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.

  1. Pick a sentence from the main content, not from the navigation.
  2. Fetch the raw HTML with an AI crawler's user agent and count matches:
bash
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"
  1. If the count is 0 but the sentence shows in the browser, the text is rendered client-side.
  2. Check whether the page streams hidden content:
bash
curl -s -A "$UA" https://example.com/pricing | grep -o '<div hidden id="S:[0-9]*"' | wc -l
  1. 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 htmlLimitedBots fix above applies. Bing documents the Chrome part of its user agent as W.X.Y.Z, the current Edge version; the number below is a placeholder and does not affect the match:
bash
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 typeRecommended strategyWhy
Marketing or brochure siteSSGContent changes on deploy; full HTML from a CDN at the lowest cost
Blog, docs, help centreSSG, rebuilt or revalidated on publishLong text that must be in the HTML; changes are infrequent and known
E-commerce catalogueISR with on-demand revalidation, or cached SSRPrice and stock must be in the HTML and current; keep cart and account client-side
News or fast-moving listingsSSR with short CDN cachingFreshness matters more than render cost
Large marketplace (millions of URLs)SSR or ISR generated on first requestBuilding every page at deploy is impractical
Logged-in SaaS appCSR is fine for the app; SSG for the public site and docsCrawlers cannot log in, so the app does not need to be readable
Existing SPA you cannot rewritePrerender public routes at build timeReact 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.

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