Client-Side Rendering vs SSR vs Hybrid
The architectural difference between rendering modes determines whether you scrape with curl, parse a hydration payload, or drive a browser.
What you’ll learn
- Define CSR, SSR, SSG, ISR, and the modern hybrid model in one sentence each.
- Identify the rendering mode of a real page from the response and view-source.
- Map each rendering mode to a scraper strategy.
- Recognise the framework fingerprints, Next.js, Nuxt, Remix, SvelteKit, Astro.
The single biggest factor in choosing a scraper strategy is where the HTML is generated: on the server, in the browser, or both. Knowing the rendering mode of a page is what separates a thirty-second job from a thirty-minute one.
Five rendering modes in one sentence each
| Mode | What happens |
|---|---|
| SSR (Server-Side Rendering) | Every request runs through the framework on the server; HTML comes back complete. |
| CSR (Client-Side Rendering) | Server returns a near-empty shell; the browser fetches data and builds the DOM. |
| SSG (Static Site Generation) | HTML is pre-rendered at build time and served as static files; effectively SSR for the scraper. |
| ISR (Incremental Static Regeneration) | SSG, but pages re-render on a schedule or trigger; still looks like SSR from outside. |
| Hybrid (SSR + hydration) | Server renders HTML and embeds a JSON payload; the browser "hydrates" markup into an interactive app. |
From a scraper's perspective, SSR, SSG, and ISR all look the same: data is in the HTML. CSR is the hard case. Hybrid is a gift, the data is in the response as JSON, easier to parse than HTML.
How to spot the mode in one minute
Open the page, View Source, and look at the first 200 lines:
- Massive amount of rendered HTML (product cards, articles, prices) → SSR/SSG/ISR. Scrape with
requests. <div id="root"></div>or<div id="__next"></div>with no content inside → CSR. Use a browser or find the API.- Rendered HTML plus a giant
<script id="__NEXT_DATA__">{...}</script>→ Hybrid. Parse the JSON. <script id="serialized-state">orwindow.__INITIAL_STATE__ = {...}→ also hybrid. Different framework, same idea.
These signatures are not subtle. Three minutes with View Source on any major site teaches you the patterns.
Framework fingerprints
Each major framework leaves an obvious mark:
| Framework | Telltale | Default mode |
|---|---|---|
| Next.js | <script id="__NEXT_DATA__">, /_next/ static assets |
SSR/SSG/ISR/Hybrid |
| Nuxt | <script>window.__NUXT__={...}</script> |
SSR/Hybrid |
| Remix | <script>window.__remixContext = {...}</script> |
SSR |
| SvelteKit | <script>__sveltekit_*</script> |
SSR/SSG/Hybrid |
| Astro | data-astro-* attributes, no big JSON |
SSG (mostly) |
| Gatsby | <script id="___gatsby">, window.pageData |
SSG |
| Pure React/Vue SPA | <div id="root"> (or #app), no SSR payload |
CSR |
When you see one of these, you immediately know what tool to reach for. A Next.js site? Parse __NEXT_DATA__ and skip the browser. A pure React SPA? Find the JSON API the app calls.
What hybrid actually means
A modern Next.js page does both. The server runs your React code, produces HTML, and embeds the data (props for that page) as JSON in a <script> tag. The browser then "hydrates", wires up event handlers and state without re-fetching.
This is enormous for scrapers. Look at any /products/{slug} page on a Next.js e-commerce site:
<html>
<body>
<div id="__next">
<article>
<h1>Yellow Ceramic Mug</h1>
<p>$12.00</p>
<!-- ...full rendered product card... -->
</article>
</div>
<script id="__NEXT_DATA__" type="application/json">
{"props":{"pageProps":{"product":{"id":1,"name":"Yellow Ceramic Mug","price":1200,"sku":"YCM-001","variants":[...],"description":"..."}}}}
</script>
</body>
</html>
You could parse the HTML. But the __NEXT_DATA__ payload contains richer data than what's rendered, typed numbers, IDs, variants, internal fields the UI doesn't even display. Always check the hydration payload first.
import json, re, requests
from bs4 import BeautifulSoup
r = requests.get("https://practice.scrapingcentral.com/products/1-white-wooden-vase")
soup = BeautifulSoup(r.text, "lxml")
payload = json.loads(soup.select_one("#__NEXT_DATA__").string)
product = payload["props"]["pageProps"]["product"]
print(product["name"], product["price"], product["sku"])
Three lines, no browser, the full product record.
When the mode determines the tool
| If the page is... | Scraper strategy |
|---|---|
| Pure SSR/SSG/ISR | requests + BeautifulSoup, or Guzzle + DomCrawler |
| Hybrid with hydration payload | requests + JSON parse, fastest of all |
| Pure CSR with a discoverable JSON API | requests to the API directly (Sub-Path 4) |
| Pure CSR with no discoverable API | Playwright/Selenium |
| Pure CSR with anti-bot fingerprinting | Stealthed Playwright or a SERP API |
Browser automation is the last resort, not the default. The art of dynamic-web scraping is recognising when you can avoid it.
Catalog108 examples
/products, server-rendered grid. Pure SSR.curlreturns everything./products/1-white-wooden-vase, hybrid Next.js-style with__NEXT_DATA__. Parse the JSON./challenges/dynamic/spa-pure, pure CSR. View source is empty. Browser required./challenges/dynamic/spa-routed, pure CSR with client-side routing. Browser required.
Run View Source on each and compare. You will see the four shapes inside ten minutes.
Hands-on lab
Visit /products and confirm via View Source that the grid is server-rendered. Then visit /products/1-white-wooden-vase and find the hydration payload (search the source for __NEXT_DATA__ or application/json). Write a curl | python -c '...' one-liner that extracts the product's SKU from the payload. No browser needed.
Hands-on lab
Practice this lesson on Catalog108, our first-party scraping sandbox.
Open lab target →/productsQuiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.