React.js Fundamentals
Server-Side Rendering Concepts
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
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
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)