Server-Side vs Client-Side Rendering Cheat Sheet
Compares SSR, CSR, SSG, and ISR rendering strategies with real code examples and guidance on which to pick for a given use case.
SSR vs CSR vs SSG vs ISR
The four main rendering strategies for the web.
- SSR- HTML generated per-request on the server, sent fully rendered to the browser
- CSR- Browser downloads a minimal HTML shell + JS bundle, then renders via JavaScript
- SSG- HTML pre-built at build time; served as static files from a CDN
- ISR- Static pages regenerated in the background after a set revalidation interval
- Hydration- Client-side JS attaches event listeners to server-rendered HTML to make it interactive
- TTFB vs TTI- SSR/SSG improve first paint; CSR often delays Time to Interactive
Server-Side Rendering (Next.js)
Data fetched and HTML rendered on every request.
// pages/product/[id].jsexport async function getServerSideProps({ params }) { const res = await fetch(`https://api.example.com/products/${params.id}`); const product = await res.json(); return { props: { product } }; // runs on every request, on the server}export default function ProductPage({ product }) { return <h1>{product.name}</h1>; // HTML is fully formed before it reaches the browser}
Client-Side Rendering (React SPA)
Data fetched and rendered entirely in the browser.
// Plain React SPA — index.html has just <div id="root"></div>import { useEffect, useState } from 'react';function ProductPage({ id }) { const [product, setProduct] = useState(null); useEffect(() => { fetch(`/api/products/${id}`) .then((res) => res.json()) .then(setProduct); // data + render happen entirely client-side }, [id]); if (!product) return <p>Loading...</p>; // blank/skeleton until JS runs return <h1>{product.name}</h1>;}
When to Choose Which
Matching the rendering strategy to the use case.
- Choose CSR- Highly interactive apps behind login (dashboards, admin panels) where SEO doesn't matter
- Choose SSR- Content that must be indexable and fresh per-request (e.g. personalized feeds)
- Choose SSG- Marketing pages, blogs, docs — infrequently changing content, cacheable at the CDN
- Choose ISR- Large catalogs where full rebuilds are too slow but content needs periodic freshness
- SEO- SSR/SSG give crawlers fully-formed HTML; CSR relies on crawlers executing JS
Streaming SSR with React Suspense
Sending HTML in chunks as data becomes ready instead of waiting for the full page.
// app/dashboard/page.js (Next.js App Router, React Server Components)import { Suspense } from 'react';async function SlowWidget() { const data = await fetch('https://api.example.com/stats', { cache: 'no-store' }).then(r => r.json()); return <StatsPanel data={data} />;}export default function DashboardPage() { return ( <div> <h1>Dashboard</h1> {/* Shell + fast content flush immediately; SlowWidget streams in later */} <Suspense fallback={<Skeleton />}> <SlowWidget /> </Suspense> </div> );}// The server keeps the HTTP response open and flushes additional <template>/script// chunks that swap the fallback for real markup as each Suspense boundary resolves —// no client-side waterfall, no single slow query blocking the whole page.
Islands Architecture (Partial Hydration)
Shipping mostly static HTML with isolated interactive 'islands' instead of hydrating the whole page.
---// Astro component — server-rendered by default, zero client JS shippedimport Counter from '../components/Counter.jsx';const posts = await getCollection('blog');---<html> <body> <!-- Static, server-rendered — never hydrated --> <BlogList posts={posts} /> <!-- Only this island ships JS and hydrates independently --> <Counter client:visible initialCount={0} /> </body></html><!-- client:load -> hydrate immediately on page load client:idle -> hydrate when the main thread is idle (requestIdleCallback) client:visible -> hydrate only when scrolled into view (IntersectionObserver)-->
Advanced Rendering Vocabulary
Terms that show up once you move past the basic SSR/CSR/SSG split.
- RSC (React Server Components)- Components that render only on the server, never ship their own JS to the client, and can read backends/DB directly
- Selective/resumable hydration- Hydration is prioritized per-component based on user interaction (e.g. React's hydrateRoot with Suspense) instead of one big blocking pass
- Islands architecture- Static HTML by default; only explicitly marked interactive components hydrate (Astro, Fresh, Marko)
- Edge SSR- Server rendering executed in edge/CDN runtimes (Vercel Edge, Cloudflare Workers) close to the user, cutting TTFB
- Waterfall vs parallel fetch- Sequential await calls in CSR create request waterfalls; SSR frameworks let you fetch in parallel before render
- Hydration mismatch- Server-rendered markup differs from the client's first render pass (e.g. Date.now(), window checks), causing React to warn and re-render
- PPR (Partial Prerendering)- A single response mixes a statically prerendered shell with dynamically streamed segments
Avoiding Hydration Mismatches
Guarding client-only values so server and client render the same initial markup.
import { useEffect, useState } from 'react';function ClientOnlyClock() { const [mounted, setMounted] = useState(false); // Only true after the client has taken over — server never sees this branch useEffect(() => { setMounted(true); }, []); if (!mounted) return <span suppressHydrationWarning>--:--:--</span>; return <span>{new Date().toLocaleTimeString()}</span>;}// Common mismatch causes to avoid rendering directly in the render body:// - Date.now() / Math.random() without seeding// - typeof window !== 'undefined' branches that change markup// - Locale-dependent formatting that differs between server and browser timezone
Resumability (Qwik-style Zero Hydration)
An alternative to hydration: serialize execution state in HTML and resume listeners lazily on interaction.
// Qwik component — no hydration pass at allimport { component$, useSignal } from '@builder.io/qwik';export const Counter = component$(() => { const count = useSignal(0); return ( <button onClick$={() => count.value++}> Clicks: {count.value} </button> );});// The server serializes component state + a pointer to the click handler's// module chunk directly into the HTML. The browser downloads ZERO JS until// the user actually clicks — at that point only the handler chunk loads.// This sidesteps the 'download + parse + execute everything, then hydrate'// cost that both CSR and traditional SSR pay on every page load.
Don't treat SSR vs CSR as all-or-nothing — most production apps mix strategies per route (SSG for marketing pages, CSR for the authenticated dashboard) using a meta-framework like Next.js, Nuxt, or Remix.