Skip to content
Episode 15 · The spinner Google can't read

CSR vs SSR vs SSG vs ISR: where and when the HTML gets built

Monday, 10:05: on a 4G phone, a StayHub hotel page shows a spinner for 2.8 s before any hotel appears, and Google lists all 1,500 hotel pages as just "StayHub", with no description.

Every page's HTML gets built somewhere: in the browser, on the server for each visit, or once in advance. Where and when changes speed, freshness and cost.

30 min intermediateCSR vs SSR vs SSG vs ISRHydration: making server HTML interactiveIncremental static regeneration and freshness windowsCDN edge cachingCachingWhat the frontend is responsible for
  1. Problem

    A hotel site has marketing pages, a blog, hotel pages with live availability and a logged-in dashboard. Each must load fast, rank on search engines where it matters, and show correct data.

  2. Why the simple system fails

    One rendering strategy for everything fails somewhere: a client-rendered SPA shows spinners and empty HTML to crawlers; rendering every page on the server for every request is slow and expensive; static pages go stale.

  3. Engineering concept

    Decide per route WHERE the HTML is produced (browser, server per request, build time, or cached and regenerated) and WHEN the data is fetched. Hydration then makes server HTML interactive.

  4. Technology

    CSR (React SPA), SSR, SSG, ISR, hydration, CDNs, and frameworks such as Next.js that let each route choose.

  5. Trade-offs

    Earlier HTML means faster first content and better SEO but staler data or more server cost. Fresher data means more work per request. Every server-rendered page still pays a hydration cost before it responds to clicks.

01

The problem

2 min
Monday, 10:05. Hotel pages were rewritten as a React app last month.
  1. Maya · Product manager
    On 4G, hotel pages show a spinner for 2.8 s. And Google lists all 1,500 as just "StayHub", no description.
  2. You · Engineer at StayHub
    Two different problems?
  3. Lina · Senior engineer, your mentor
    Maybe one. What does Google get when it fetches a hotel page?
02

The big idea

4 min
Who builds the HTML?
The big idea

The real question is who builds the HTML, and when. Decide it per page.

Not which framework. A smaller bundle would help a bit, but couldn't fix Google, and the phone would still wait for JavaScript, then for data.

03

How it works

5 min
Four answers to "who builds the HTML, and when?"
  1. 1
    CSR

    : an empty shell, then JS (until 2.2 s), then a data fetch: hotel visible at 2.8 s on 4G. Crawlers see an empty box.

  2. 2
    SSR

    : Frankfurt fetches the hotel and builds HTML per request: visible at 0.9 s. The server works on every view.

  3. 3
    SSG

    : built once at deploy, served from the CDN: visible at 0.4 s. Frozen until the next build.

  4. 4
    ISR

    : like SSG, but rebuilt in the background at most every N minutes. Briefly stale, then fresh.

  5. 5
    Hydration

    : for SSR, SSG and ISR, the JS still loads and attaches click handlers. Until then the page looks ready but ignores taps: about 2 s.

SSR, SSG and ISR all fix Google: the hotel is in the first HTML. They differ in freshness and cost: SSR always fresh but works every view; SSG free to serve but frozen; ISR cheap and stale for at most its window. lets each route choose. Lina's proposal for hotel pages: ISR for description, photos and reviews; the live price fetched separately.

04

Watch a decision being made

4 min
Worked example

Lina and you pick a strategy per route

Home page, 1,500 hotel pages, search results, "My bookings". Pick where and when each page's HTML is built: fast, on Google where needed, and correct.

  1. 1Ask the two questions per route

    How fresh must the data be? And who must see the content before JavaScript runs (Google, link previews, slow phones)?

    Why: These answers decide the strategy, not the framework or fashion.

  2. 2Match each route

    Home → SSG. Hotel pages → ISR (10 min). Search → SSR (endless combinations, must be fresh). My bookings → CSR behind login.

    tsx
    // app/hotels/[id]/page.tsx
    export const revalidate = 600;            // ISR: rebuild at most every 10 min
    // app/search/page.tsx
    export const dynamic = "force-dynamic";   // SSR: render per request

    Why: One strategy everywhere makes a page either stale or expensive.

  3. 3Split out the live part

    The price changes every few minutes: a small client component fetches /api/price?hotel=42 after the page appears.

    Why: One live number shouldn't put the whole page on the server for every view.

  4. 4Check cost at peak, measure on 4G

    ~24,000 views a day: SSR means 24,000 renders and queries; ISR, about 3,000 rebuilds whatever the traffic. Hotel visible at 0.4 s (was 2.8), buttons at ~1.8 s.

    Why: SSR cost grows with traffic. Measuring "visible" and "interactive" catches the hydration gap.

The whole thing in one line: Pre-build the bulk, fetch the live parts. Choose per route from freshness and who needs the HTML early.
05

In real life

3 min
Two well-known apps, two choices

Gmail: client-rendered, behind login

  • Viewing the page source shows mostly scripts, not your emails
  • Private data, no search engines, long sessions
  • Slower first load, then instant

CSR fits private, app-like pages kept open for hours.

Amazon product pages: content in the first HTML

  • The page source shows title, description and price in the HTML
  • Search engines and link previews read it without JavaScript

Server-built HTML fits public pages that must rank and appear fast.

06

What goes wrong

3 min
Thinking SSR makes the page clickable sooner

Content shows sooner; taps still wait for JS and hydration.

Instead

Measure "visible" and "interactive"; ship less client JavaScript.

Baking volatile data into static pages

"3 rooms left" in a pre-built page is wrong within minutes.

Instead

Fetch it live, and re-check on the server at checkout.

Rendering browser-only values on the server

Time, random ids or window size differ: hydration mismatches.

Instead

Compute them after hydration (useEffect).

07

Trade-offs

3 min
StrategyFitsYou payMain risk
CSRBehind login, no SEOSpinners until JS + dataInvisible to crawlers
SSRPer-request pages (search)Server and DB work every viewSpikes hit the database
SSGChanges with deploysRebuildsStale content
ISRMany pages changing hourlyBounded stalenessConfusing revalidation
Friday, 11:00. How hotel pages show availability.
  1. Maya · Product manager
    I want "rooms left" in the page itself, always right. SSR every hotel page, please.
  2. Omar · On-call SRE
    We just saw SSR take the database down. ISR, and fetch rooms left live.
  3. Maya · Product manager
    Then the number pops in later, and Google sees no availability.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • The rendering strategy per kind of page: public hotel pages, search results and the private account area each get their own answer.
  • How stale a pre-built page may be (10 minutes for descriptions) and which values never come from it: price and rooms left, fetched live and re-checked at checkout.
  • What a traffic spike may cost: you choose what the CDN serves when the newsletter goes out.
Tell Claude Code
StayHub's 1,500 hotel pages are client-rendered React: on 4G the hotel appears after 2.8 s, and Google lists them as "StayHub" with no description. Switch hotel pages to pre-rendered pages from the CDN, regenerated at most every 10 minutes (ISR). Price and rooms left load live in a small separate component. Keep search and the account area as they are. Plan first, including a 40× newsletter spike. Verify with a throttled 4G trace and the raw HTML of one hotel page.
Check before you accept
  • "View source" on a hotel page: the name and description are in the HTML itself.
  • The price is not baked into the cached page.
  • The 4G trace shows the hotel at ~0.4 s, and when Book starts working.
Red flags
  • SSR on every request with no caching, so a spike lands on the shared database.
  • "2 rooms left" or the price inside a page cached for minutes.
  • "SEO fixed" without showing the HTML a crawler receives.
09

Check yourself

2 min
Q1/3Decision
Match each route to the strategy that fits it best:
Engineering blog, updated weekly, must rank on Google
Admin dashboard behind login, data changes every second
Search results for any city + dates + filters
50,000 hotel description pages, edited a few times a day
How confident are you?

Next episode: a guest changes her dates and the basket still shows the old total, and shared search links open an empty page. Where should each piece of state live?

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

A pre-built page with a live price

In a Next.js App Router project, each route declares how it's rendered. APIs evolve between versions, so read these as decisions, and check your version's docs for exact syntax.

Worked example

Lina and you pick a strategy for each StayHub route

StayHub has four kinds of pages: the home page, 1,500 hotel pages, search results, and "My bookings". Pick where and when each page's HTML is built, so pages appear fast, rank on Google where needed, and stay correct.

  1. 1Ask the two questions per route

    For each route: how fresh must the data be? and who must see the content before JavaScript runs (Google, link previews, slow phones)?

    Why: These two answers decide the strategy. The framework, or what's fashionable, doesn't.

  2. 2Sort routes by freshness

    Home: changes with deploys. Hotel descriptions and photos: a few times a week. Prices: every few minutes. Search results: every combination of city and dates is different. My bookings: personal.

    Why: "How often does this change?" tells you how long a copy can live: forever, minutes, or not at all.

  3. 3Match each route to a strategy

    Home → SSG. Hotel pages → ISR (revalidate 600 s). Search → SSR (endless combinations, must be fresh). My bookings → CSR behind login (private, no SEO).

    tsx
    // app/hotels/[id]/page.tsx
    export const revalidate = 600;            // ISR: rebuild at most every 10 min
    
    // app/search/page.tsx
    export const dynamic = "force-dynamic";   // SSR: render per request

    Why: Each strategy fits one freshness level. Using one for everything means a page is either stale or expensive.

  4. 4Split out the live part

    The price is the one value on a hotel page that changes every few minutes. Make it a small client component that fetches /api/price?hotel=42 after the page appears.

    tsx
    "use client";
    export function LivePrice({ hotelId }: { hotelId: string }) {
      const [price, setPrice] = useState<number | null>(null);
      useEffect(() => {
        fetch("/api/price?hotel=" + hotelId).then((r) => r.json()).then((d) => setPrice(d.price));
      }, [hotelId]);
      return <span>{price === null ? "Checking price…" : "€" + price + " / night"}</span>;
    }

    Why: One live number shouldn't force the whole page onto the server for every view.

  5. 5Check cost at peak

    Today hotel pages get ~24,000 views a day. SSR: 24,000 renders and database queries. ISR: at most one rebuild per visited hotel every 10 minutes, about 3,000 a day, whatever the traffic.

    Why: SSR cost grows with traffic; ISR cost grows with the number of pages and the window. Spikes are where the difference hurts.

  6. 6Measure on a 4G phone

    Hotel visible at 0.4 s (was 2.8 s), price at about 0.7 s, buttons working at about 1.8 s. Then check Google's view of the page with a URL inspection tool.

    Why: Measuring both "visible" and "interactive" catches the hydration gap that a single number hides.

The whole thing in one line: Lina and you chose per route from two questions (freshness, who needs the HTML early): SSG for home, ISR plus a live price for hotels, SSR for search, CSR for bookings. Hotel pages went from 2.8 s to 0.4 s and Google can read them.
tsx
// Hydration mismatch: the server renders one time, the browser another.
function Greeting() {
  return <p>Rendered at {new Date().toLocaleTimeString()}</p>; // differs server vs client
}

// Fix (in a client component): render time-dependent values after hydration.
function GreetingFixed() {
  const [now, setNow] = useState<string | null>(null);
  useEffect(() => setNow(new Date().toLocaleTimeString()), []);
  return <p>Rendered at {now ?? "…"}</p>;
}
Anything that differs between server and browser (time, random ids, window size, localStorage) belongs after hydration.
CSRSSRSSGISR
HTML builtIn the browserOn the server, per requestAt build timeAt build, then regenerated after a window
First real contentLate (JS + data fetch)EarlyEarliestEarliest
Data freshnessLiveLiveAs of last buildAt most N seconds stale
SEO / link previewsWeakGoodGoodGood
Server cost per viewLow (API calls only)HighestNear zeroNear zero (+ periodic re-render)
Typical useDashboards behind loginSearch, personalised pagesDocs, marketingProduct pages, catalogues
11

Interactive simulation

12

Visual diagram

Click a component

Where the HTML is produced under each strategy. Start the walkthrough.

13

Analogy

  • Hotel brochuresA flat-pack kit you assemble yourselfRenderingCSR: the browser builds it
  • Hotel brochuresConcierge printing a fresh brochure for youRenderingSSR: built per request
  • Hotel brochuresA stack of pre-printed brochuresRenderingSSG: built once, served from a CDN
  • Hotel brochuresSomeone reprinting the stack every hourRenderingISR: rebuilt in the background
  • Hotel brochuresBrochure becoming a working booking formRenderingHydration

Where the analogy breaks: you can't mix brochure types on one page. In code you can: pre-build most of a hotel page and load the live price and availability separately.

14

Check your understanding

Q1/3Scenario
A startup's React SPA (pure CSR) gets almost no organic search traffic, and shared links show no preview. Which change addresses this most directly?
How confident are you?
15

Practice ladder

Step 1 · Trace

Trace a hotel page after an edit

Scenario Warm-up

The hotel page /hotels/42 uses ISR with revalidate = 600 (10 minutes) and a live price component.

  • 09:00:00 The page is rebuilt.
  • 09:04:00 The hotel owner changes the description.
  • 09:12:00 Guest A opens the page.
  • 09:12:05 Guest B opens the page.
  • 09:12:30 The price changes in the database.
  • 09:12:31 Guest C opens the page.

For each guest, say which description they see and where the price comes from.

Step 2 · Complete

Finish Lina's route table

Design Practice

Lina's note on StayHub's routes has two gaps. Fill in the strategy and the reason for rows 3 and 4.

#RouteStrategyWhy
1/ homeSSGSame for everyone, changes with deploys
2/hotels/[id]ISR 600 s + live pricePublic, SEO, content changes a few times a week
3/search?city=&from=&to=______
4/account/bookings______
Step 3 · Solve

The travel guide that never updates

Debugging Practice

StayHub launched a travel guide: 300 city pages ("Things to do in Lisbon"). They're SSG, built at deploy. Editors complain: "I fixed a typo on Monday, it's still live on Thursday." We deploy about once a week.

Meanwhile Karim suggests switching the guide to SSR "so edits show instantly".

Explain why edits are slow to appear, and propose a better fix than SSR. Say what it costs.

Step 4 · Decide

Flash sale on the Lisbon page

Decision Stretch

Maya plans a 24-hour flash sale: prices on 50 Lisbon hotels drop 30%, and the newsletter goes to 200,000 people at 09:00. She wants the sale price shown correctly the moment the sale starts.

Hotel pages today: ISR (600 s) with the price fetched live from /api/price, which queries PostgreSQL.

Option A: switch the 50 sale pages to SSR for the day. Option B: keep ISR, revalidate the 50 pages on demand at 09:00, and cache /api/price responses on the CDN for 30 seconds.

Write a short decision: "We chose X over Y because…, accepting the risk of…".

16

Explain it yourself

Explain to a product manager why the hotel detail page shouldn't be pure CSR or pure SSG, and what you'd do instead.

0 words