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.
- 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.
- 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.
- 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.
- Technology
CSR (React SPA), SSR, SSG, ISR, hydration, CDNs, and frameworks such as Next.js that let each route choose.
- 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.
The problem
2 min- Maya · Product managerOn 4G, hotel pages show a spinner for 2.8 s. And Google lists all 1,500 as just "StayHub", no description.
- You · Engineer at StayHubTwo different problems?
- Lina · Senior engineer, your mentorMaybe one. What does Google get when it fetches a hotel page?
The big idea
4 minThe 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.
How it works
5 min- 1CSR
: 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.
- 2SSR
: Frankfurt fetches the hotel and builds HTML per request: visible at 0.9 s. The server works on every view.
- 3SSG
: built once at deploy, served from the CDN: visible at 0.4 s. Frozen until the next build.
- 4ISR
: like SSG, but rebuilt in the background at most every N minutes. Briefly stale, then fresh.
- 5Hydration
: 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.
Watch a decision being made
4 minLina 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.
- 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.
- 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 requestWhy: One strategy everywhere makes a page either stale or expensive.
- 3Split out the live part
The price changes every few minutes: a small client component fetches
/api/price?hotel=42after the page appears.Why: One live number shouldn't put the whole page on the server for every view.
- 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.
In real life
3 minGmail: 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.
What goes wrong
3 minContent shows sooner; taps still wait for JS and hydration.
Measure "visible" and "interactive"; ship less client JavaScript.
"3 rooms left" in a pre-built page is wrong within minutes.
Fetch it live, and re-check on the server at checkout.
Time, random ids or window size differ: hydration mismatches.
Compute them after hydration (useEffect).
Trade-offs
3 min| Strategy | Fits | You pay | Main risk |
|---|---|---|---|
| CSR | Behind login, no SEO | Spinners until JS + data | Invisible to crawlers |
| SSR | Per-request pages (search) | Server and DB work every view | Spikes hit the database |
| SSG | Changes with deploys | Rebuilds | Stale content |
| ISR | Many pages changing hourly | Bounded staleness | Confusing revalidation |
- Maya · Product managerI want "rooms left" in the page itself, always right. SSR every hotel page, please.
- Omar · On-call SREWe just saw SSR take the database down. ISR, and fetch rooms left live.
- Maya · Product managerThen the number pops in later, and Google sees no availability.
Your job as tech lead
4 minClaude 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.
- 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.
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.
- "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.
- 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.
Check yourself
2 minNext 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?
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.
Technical deep dive
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.
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.
- 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.
- 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.
- 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 requestWhy: Each strategy fits one freshness level. Using one for everything means a page is either stale or expensive.
- 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=42after 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.
- 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.
- 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.
// 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>;
}| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| HTML built | In the browser | On the server, per request | At build time | At build, then regenerated after a window |
| First real content | Late (JS + data fetch) | Early | Earliest | Earliest |
| Data freshness | Live | Live | As of last build | At most N seconds stale |
| SEO / link previews | Weak | Good | Good | Good |
| Server cost per view | Low (API calls only) | Highest | Near zero | Near zero (+ periodic re-render) |
| Typical use | Dashboards behind login | Search, personalised pages | Docs, marketing | Product pages, catalogues |
Interactive simulation
Visual diagram
Where the HTML is produced under each strategy. Start the walkthrough.
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.
Check your understanding
Practice ladder
Trace a hotel page after an edit
Scenario Warm-upThe 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.
Finish Lina's route table
Design PracticeLina's note on StayHub's routes has two gaps. Fill in the strategy and the reason for rows 3 and 4.
| # | Route | Strategy | Why |
|---|---|---|---|
| 1 | / home | SSG | Same for everyone, changes with deploys |
| 2 | /hotels/[id] | ISR 600 s + live price | Public, SEO, content changes a few times a week |
| 3 | /search?city=&from=&to= | ___ | ___ |
| 4 | /account/bookings | ___ | ___ |
The travel guide that never updates
Debugging PracticeStayHub 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.
Flash sale on the Lisbon page
Decision StretchMaya 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…".
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.