DNA🌐 Web BrowserSSR vs CSR vs Universal Rendering
πŸ¦–DinosaurBrowserArchitecturePerformanceSSR

SSR vs CSR vs Universal Rendering

The rendering strategy you choose defines your app's performance ceiling, SEO story, and infrastructure complexity. This isn't a syntax decision β€” it's an architecture decision.

SSR vs CSR vs Universal Rendering

Every frontend architect faces the same question: where and when does your HTML get generated? The answer determines your Time to First Byte, Time to Interactive, SEO capabilities, hosting costs, and caching strategy.

The Spectrum of Rendering

                Server-side                          Client-side
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                                                          β”‚
   SSG        ISR          SSR       Streaming    Universal   CSR/SPA
 (Static)  (Incremental) (Dynamic)   SSR        (Hydration)
    β”‚                                                          β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  Fastest FCP                                        Slowest FCP
  Stale data                                         Fresh data
  Cheapest                                           Most flexible

Client-Side Rendering (CSR / SPA)

The browser receives a minimal HTML shell and JavaScript builds the entire UI:

<!-- What the server sends -->
<!DOCTYPE html>
<html>
<body>
  <div id="root"></div>
  <script src="/app.js"></script>  <!-- 200KB+ -->
</body>
</html>

The Timeline

Server sends HTML shell ─→ Browser downloads JS ─→ JS executes ─→ API calls ─→ Render
      β”‚                          β”‚                      β”‚              β”‚           β”‚
      └──── Empty page β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”€β”€β”€β”€ Still empty β”€β”€β”€β”€β”€β”€β”˜β”€β”€ Spinner β”€β”€β”˜β”€β”€ Content β”‚

When CSR Works

  • Behind authentication (no SEO needed)
  • Highly interactive dashboards
  • Real-time applications (trading, gaming)
  • When you want simple static hosting (S3, Netlify)

When CSR Fails

  • SEO β€” Search crawlers may not execute JavaScript (Google does, others may not)
  • First Contentful Paint β€” User sees nothing until JS downloads, parses, and executes
  • Low-end devices β€” Heavy JS payload on a $100 phone on 3G = 10+ second load
  • Social sharing β€” Open Graph tags need to be in the initial HTML

CSR Performance Profile

FCP:  Slow (waiting for JS download + execution)
LCP:  Very slow (waiting for JS + API call + render)
TTI:  Moderate (once JS is loaded, it's already interactive)
TTFB: Fast (just serving a static file)

Server-Side Rendering (SSR)

The server generates full HTML for every request:

Browser requests /products
    β†’ Server fetches data from DB/API
    β†’ Server renders React to HTML string
    β†’ Sends complete HTML to browser
    β†’ Browser displays content immediately
    β†’ JS downloads and "hydrates" (attaches event handlers)

The Timeline

Server renders HTML ──→ Browser receives full HTML ──→ JS downloads ──→ Hydration
      β”‚                        β”‚                            β”‚              β”‚
      └── Server processing β”€β”€β”˜β”€β”€β”€β”€ Content visible! β”€β”€β”€β”€β”€β”€β”˜β”€β”€ Not yet β”€β”€β”˜β”€β”€ Interactive

Traditional SSR Code

// Next.js Pages Router (getServerSideProps)
export async function getServerSideProps(context) {
  const products = await db.products.findAll();
  return { props: { products } };
}
 
export default function ProductsPage({ products }) {
  return (
    <div>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

SSR Trade-offs

Advantages:

  • Full HTML on first response β€” great FCP and LCP
  • SEO-friendly β€” crawlers get complete content
  • Social sharing works (OG tags in initial HTML)
  • Works without JavaScript (progressive enhancement)

Disadvantages:

  • TTFB is slower β€” Server must fetch data and render before responding
  • Server cost β€” Every request hits your server (compute cost scales with traffic)
  • Time to Interactive gap β€” User sees content but can't interact until hydration completes
  • No caching (by default) β€” Every request is unique

SSR Performance Profile

FCP:  Good (full HTML arrives quickly)
LCP:  Good (content is in the HTML)
TTI:  Moderate (hydration delay β€” the "uncanny valley")
TTFB: Slower (server must render)

Static Site Generation (SSG)

HTML is generated at build time, not request time:

Build time: Fetch data β†’ Render HTML β†’ Save as static files
Request time: Serve pre-built HTML from CDN β†’ Instant
// Next.js App Router (static by default)
async function BlogPost({ params }) {
  const post = await getPost(params.slug); // Called at build time
  return <Article post={post} />;
}
 
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

SSG Performance Profile

FCP:  Excellent (pre-built HTML from CDN edge)
LCP:  Excellent (all content is static)
TTI:  Good (less JS to hydrate)
TTFB: Excellent (CDN cache hit)
Cost: Minimal (static files, CDN-served)

When SSG Breaks

  • Content changes frequently (requires rebuild for every change)
  • Thousands of pages (build time grows linearly)
  • Personalized content (same page for everyone)

Incremental Static Regeneration (ISR)

The best of SSG and SSR β€” pages are statically generated but can be revalidated:

// Next.js β€” revalidate every 60 seconds
export const revalidate = 60;
 
async function ProductsPage() {
  const products = await fetchProducts();
  return <ProductGrid products={products} />;
}

How ISR Works

First request:  Build page β†’ Cache β†’ Serve from cache
Next 60 seconds: Serve stale cache (instant)
After 60s:       Serve stale cache AND trigger background rebuild
Next request:    Serve newly built page from cache

This is stale-while-revalidate at the page level. Users always get instant responses; data staleness is bounded by the revalidation interval.

Streaming SSR

Instead of waiting for all data before sending HTML, the server streams content progressively:

// React 18 + Next.js App Router
async function Dashboard() {
  return (
    <div>
      <Header />                               {/* Sent immediately */}
      <Suspense fallback={<MetricsSkeleton />}>
        <MetricsPanel />                        {/* Streams when data ready */}
      </Suspense>
      <Suspense fallback={<ChartSkeleton />}>
        <ChartSection />                        {/* Streams independently */}
      </Suspense>
    </div>
  );
}

Streaming Timeline

Traditional SSR:
[Wait for ALL data...............] β†’ [Send complete HTML] β†’ [Hydrate]
 
Streaming SSR:
[Send shell + skeleton] β†’ [Stream metrics] β†’ [Stream chart] β†’ [Selective hydrate]
     Instant FCP             Progressive            Progressive

Selective Hydration

React hydrates streamed sections independently. If the user clicks on a not-yet-hydrated section, React prioritizes hydrating that section first.

Universal (Isomorphic) Rendering

Universal rendering means the same JavaScript code runs on both server and client:

  1. Server renders initial HTML (SSR) for fast FCP
  2. Client receives HTML + JS bundle
  3. Client hydrates β€” attaches event handlers to existing DOM
  4. Client takes over β€” subsequent navigation is client-side (SPA behavior)
First page load:  SSR β†’ Hydration β†’ SPA mode
Subsequent nav:   Client-side routing (no server round-trip)

The Hydration Problem

Hydration is the most misunderstood part:

Server HTML:    <button>Count: 0</button>
                     ↓
Client hydrates: Attach onClick to existing button
                 Verify server HTML matches client render
                     ↓
Mismatch?       Warning + client re-renders (expensive!)

Common hydration mismatch causes:

  • Using Date.now() or Math.random() during render
  • Browser-only APIs (window.innerWidth, localStorage)
  • Extension-injected DOM nodes
  • Different timezone/locale on server vs client
// ❌ Hydration mismatch
function Greeting() {
  return <p>Current time: {new Date().toLocaleTimeString()}</p>;
}
 
// βœ… Two-pass rendering
function Greeting() {
  const [time, setTime] = useState<string | null>(null);
 
  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);
 
  return <p>Current time: {time ?? 'Loading...'}</p>;
}

The Decision Matrix

FactorSSGISRSSRStreamingCSR
Time to First ByteExcellentExcellentSlowModerateExcellent
First Contentful PaintExcellentExcellentGoodGoodPoor
Time to InteractiveGoodGoodModerateGoodModerate
Data freshnessBuild timeConfigurableRequest timeRequest timeRequest time
SEOExcellentExcellentExcellentExcellentPoor
PersonalizationNoneLimitedFullFullFull
Server costNone (CDN)LowHighHighNone
ComplexityLowLowModerateHighLow

Choosing by Page Type

Marketing / Landing pages     β†’ SSG (fast, cacheable, SEO-friendly)
Blog / Documentation          β†’ SSG or ISR (content changes periodically)
Product catalog               β†’ ISR (1000s of pages, hourly updates)
Search results                β†’ SSR or Streaming (query-dependent)
User dashboard                β†’ Streaming SSR or CSR (personalized, real-time)
Settings / Profile            β†’ CSR (behind auth, no SEO)
E-commerce checkout           β†’ SSR (personalized pricing, SEO for product pages)
Real-time collaboration       β†’ CSR with WebSockets (client-side state is king)

Modern Approach: Mix Per Route

app/
β”œβ”€β”€ (marketing)/
β”‚   └── page.tsx              β†’ SSG (static landing page)
β”œβ”€β”€ blog/
β”‚   └── [slug]/page.tsx       β†’ ISR (revalidate: 3600)
β”œβ”€β”€ products/
β”‚   └── [id]/page.tsx         β†’ SSR with streaming (personalized pricing)
β”œβ”€β”€ dashboard/
β”‚   └── page.tsx              β†’ SSR with streaming (auth-gated, real-time)
└── settings/
    └── page.tsx              β†’ CSR behind auth wrapper

No single strategy fits all. Architects choose per route based on the page's data freshness needs, SEO requirements, and interactivity level.

Islands Architecture

Instead of hydrating the entire page, Islands Architecture (popularized by Astro) sends static HTML by default and only hydrates interactive "islands":

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Static HTML (zero JS)                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Interactiveβ”‚         β”‚ Interactive      β”‚  β”‚
β”‚  β”‚ Island    β”‚  Static β”‚ Island           β”‚  β”‚
β”‚  β”‚ (React)   β”‚   HTML  β”‚ (Svelte)         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                              β”‚
β”‚  Static HTML (zero JS)                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key benefits:

  • Zero JS by default β€” Static content ships no JavaScript at all
  • Partial hydration β€” Only interactive components load their framework
  • Framework-agnostic β€” Different islands can use different frameworks
  • Progressive loading β€” Islands can hydrate on idle, on visible, or on interaction
---
import Header from './Header.astro';       // Static β€” zero JS
import SearchBar from './SearchBar.tsx';   // Interactive island
import Footer from './Footer.astro';       // Static β€” zero JS
---
 
<Header />
<SearchBar client:visible />  <!-- Hydrates when scrolled into view -->
<Footer />

Best for: Content-heavy sites (blogs, docs, marketing) where interactivity is sparse.

Animating View Transitions

The View Transitions API enables smooth animated transitions between page states or navigations without a framework:

function updateContent(newContent) {
  if (!document.startViewTransition) {
    renderNewContent(newContent);
    return;
  }
 
  document.startViewTransition(() => {
    renderNewContent(newContent);
  });
}
::view-transition-old(root) {
  animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
  animation: fade-in 0.2s ease-in;
}
 
@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }

For named transitions (morph a thumbnail into a full image):

.product-card img {
  view-transition-name: product-hero;
}
.product-detail img {
  view-transition-name: product-hero;
}

MPA support: Cross-document view transitions work with @view-transition { navigation: auto; } in CSS, enabling native page transitions in multi-page apps without JavaScript frameworks.

Interview Signal

Senior candidates demonstrate:

  1. Spectrum understanding β€” Not just "SSR vs CSR" but the full range (SSG, ISR, Streaming, Islands, Universal)
  2. Trade-off fluency β€” TTFB vs FCP vs TTI, server cost vs user experience
  3. Hydration awareness β€” What it is, why mismatches happen, the "uncanny valley," partial/selective/progressive hydration
  4. Per-route thinking β€” Different strategies for different pages in the same app
  5. Modern patterns β€” Streaming SSR, Islands Architecture, View Transitions API, RSC as the next evolution