React.js Fundamentals

Server-Side Rendering Concepts

18 min Lesson 36 of 40

Understanding Server-Side Rendering (SSR)

Server-Side Rendering is a technique where React components are rendered on the server and sent as HTML to the client, rather than rendering everything in the browser. This approach has significant implications for performance, SEO, and user experience.

SSR vs CSR vs SSG

There are three main rendering strategies for React applications:

Client-Side Rendering (CSR):
  • JavaScript bundle is sent to the browser
  • React renders components in the browser
  • Initial HTML is minimal (usually just a div)
  • Content visible after JS executes
  • Best for: highly interactive apps with authenticated users
Server-Side Rendering (SSR):
  • React renders components on the server per request
  • Full HTML is sent to the browser
  • Content visible immediately
  • Then JavaScript "hydrates" the page
  • Best for: dynamic content, personalized pages, real-time data
Static Site Generation (SSG):
  • React renders components at build time
  • Pre-rendered HTML files are generated
  • Served as static files (CDN-friendly)
  • Extremely fast load times
  • Best for: blogs, documentation, marketing pages

Basic SSR Example with Express

// server.js import express from 'express'; import React from 'react'; import { renderToString } from 'react-dom/server'; import App from './App'; const app = express(); app.get('*', (req, res) => { const appHtml = renderToString(<App />); const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>SSR React App</title> </head> <body> <div id="root">${appHtml}</div> <script src="/bundle.js"></script> </body> </html> `; res.send(html); }); app.listen(3000, () => { console.log('SSR server running on port 3000'); });

Understanding Hydration

Hydration is the process where React "attaches" to server-rendered HTML, making it interactive:

// Client-side entry point import React from 'react'; import { hydrateRoot } from 'react-dom/client'; import App from './App'; // Use hydrateRoot instead of createRoot for SSR const container = document.getElementById('root'); hydrateRoot(container, <App />);
Hydration Mismatch: If the server-rendered HTML doesn't match what React expects on the client, you'll get hydration errors. Common causes include:
  • Using browser-only APIs during server rendering
  • Date/time differences between server and client
  • Random values or IDs generated during render
  • Third-party scripts modifying the DOM

SEO Benefits of SSR

Server-Side Rendering provides significant SEO advantages:

// Server route with dynamic meta tags app.get('/product/:id', async (req, res) => { const product = await fetchProduct(req.params.id); const appHtml = renderToString( <App initialData={{ product }} /> ); const html = ` <!DOCTYPE html> <html> <head> <title>${product.name} - Our Store</title> <meta name="description" content="${product.description}"> <meta property="og:title" content="${product.name}"> <meta property="og:image" content="${product.image}"> </head> <body> <div id="root">${appHtml}</div> <script> window.__INITIAL_DATA__ = ${JSON.stringify({ product })}; </script> <script src="/bundle.js"></script> </body> </html> `; res.send(html); });
SEO Benefits:
  • Instant Indexing: Search engines see full content immediately
  • Dynamic Meta Tags: Each page can have custom titles, descriptions, and Open Graph tags
  • Social Sharing: Rich previews work perfectly on Facebook, Twitter, LinkedIn
  • Better Rankings: Core Web Vitals improve with faster initial content paint

Performance Implications

SSR affects performance in different ways:

Time to First Byte (TTFB): SSR increases TTFB because the server needs to:
  • Fetch data from databases/APIs
  • Render React components to HTML
  • Send the complete HTML
Trade-off: Slower TTFB, but faster First Contentful Paint (FCP)
Time to Interactive (TTI): With SSR:
  • Content is visible immediately (HTML)
  • But not interactive until JavaScript loads and hydrates
  • Gap between FCP and TTI can confuse users
Solution: Progressive enhancement, loading states, streaming SSR

React Server Components (RSC)

React Server Components are a new paradigm that combines SSR with modern React features:

// UserProfile.server.jsx - Server Component import { db } from './database'; export default async function UserProfile({ userId }) { // This runs ONLY on the server const user = await db.users.find(userId); const posts = await db.posts.findByUser(userId); return ( <div> <h1>{user.name}</h1> <p>{user.bio}</p> {/* Client Component for interactivity */} <PostList posts={posts} /> </div> ); } // PostList.client.jsx - Client Component 'use client'; import { useState } from 'react'; export default function PostList({ posts }) { const [filter, setFilter] = useState('all'); return ( <div> <button onClick={() => setFilter('all')}>All</button> <button onClick={() => setFilter('popular')}>Popular</button> {posts .filter(post => filter === 'all' || post.popular) .map(post => <Post key={post.id} {...post} />)} </div> ); }
Server Components Benefits:
  • Zero Bundle Size: Server components don't ship JavaScript to the client
  • Direct Backend Access: Access databases, file systems, internal APIs directly
  • Automatic Code Splitting: Only client components are bundled
  • Improved Performance: Less JavaScript = faster hydration

Streaming SSR

Modern SSR frameworks support streaming, sending HTML in chunks:

import { renderToPipeableStream } from 'react-dom/server'; app.get('*', (req, res) => { const stream = renderToPipeableStream( <App />, { onShellReady() { // Send the initial shell immediately res.setHeader('Content-Type', 'text/html'); stream.pipe(res); }, onError(error) { console.error(error); res.status(500).send('Server Error'); } } ); });
Streaming Benefits:
  • Send page shell immediately (header, navigation)
  • Stream content as data becomes available
  • Users see content faster (progressive rendering)
  • Better perceived performance

When to Use Each Strategy

// CSR - Single Page Apps // Good for: Admin dashboards, authenticated apps const config = { rendering: 'client', caching: 'none', seo: 'not-critical' }; // SSG - Static Content // Good for: Blogs, docs, landing pages const config = { rendering: 'build-time', caching: 'aggressive', seo: 'critical', revalidate: 3600 // Rebuild every hour }; // SSR - Dynamic Content // Good for: E-commerce, news, personalized feeds const config = { rendering: 'per-request', caching: 'strategic', seo: 'critical', personalization: true }; // Hybrid - Mix strategies // Good for: Most production apps const config = { rendering: 'mixed', routes: { '/blog/*': 'ssg', '/dashboard/*': 'csr', '/products/*': 'ssr' } };
Exercise 1: Analyze your current React app. For each major route, determine the optimal rendering strategy (CSR, SSR, or SSG) and justify your choice based on:
  • Content update frequency
  • SEO requirements
  • Authentication needs
  • Performance goals
Exercise 2: Create a simple SSR server using Express:
  • Set up Express with React SSR rendering
  • Create a route that fetches data and renders a component
  • Pass initial data to the client for hydration
  • Handle hydration in the client bundle
Exercise 3: Compare performance metrics:
  • Build a simple page with CSR (create-react-app)
  • Build the same page with SSR (Next.js or custom)
  • Measure TTFB, FCP, LCP, and TTI for both
  • Analyze which performs better in different scenarios (fast/slow networks)