Server-Side Rendering (SSR)

HTML Generated on the Server for Every Request

⏱️ 12 minutes 🎯 Next.js Focus 🔄 Hydration Deep Dive

🖥️ What is Server-Side Rendering?

📋 Definition

Server-Side Rendering (SSR) is a web development technique where HTML pages are generated on the server for each request, then sent to the browser as fully-formed HTML documents with content already populated.

🔄 SSR Process Flow

1

Browser Request

User navigates to URL

2

Server Processing

Server fetches data & renders HTML

3

Full HTML Response

Complete page sent to browser

4

Hydration

JavaScript makes page interactive

✨ Key Characteristics

Fast First Paint

Content visible immediately as HTML is pre-rendered

🔍

SEO Friendly

Search engines can easily crawl and index content

📱

Universal Apps

Same code runs on server and client

🔄

Fresh Data

Content generated with latest data on each request

⚖️ SSR vs CSR: The Complete Picture

📊 Loading Timeline Comparison

🖥️ Server-Side Rendering

Server Processing 0-800ms
HTML Download 800-950ms
First Paint 950ms
JS Download 950-1200ms
Hydration 1200-1400ms
Interactive 1400ms

💻 Client-Side Rendering

HTML Shell 0-100ms
JS Download 100-900ms
JS Execution 900-1200ms
API Calls 1200-1500ms
First Paint 1500ms
Interactive 1500ms

📈 Core Web Vitals Comparison

First Contentful Paint (FCP)

SSR
0.8s
CSR
1.6s

Largest Contentful Paint (LCP)

SSR
1.1s
CSR
2.2s

Time to Interactive (TTI)

SSR
1.4s
CSR
1.5s

⚖️ Trade-offs Analysis

🚀 Performance

SSR Wins
  • Faster First Contentful Paint
  • Better Largest Contentful Paint
  • Immediate content visibility
CSR Wins
  • Faster subsequent navigation
  • No server processing delay
  • Better caching strategies

💰 Infrastructure

SSR Costs
  • Server compute resources
  • Higher hosting costs
  • Complex deployment
CSR Benefits
  • Static hosting (CDN)
  • Lower server costs
  • Simple deployment

💧 Hydration: Making Static HTML Interactive

🔍 What is Hydration?

Hydration is the process where JavaScript "brings to life" the static HTML sent from the server by attaching event listeners, initializing component state, and making the page interactive.

🔄 The Hydration Process

1. Static HTML Arrives

<button> Click me </button>
<div> Count: 0 </div>
🚫 Not Interactive

2. JavaScript Downloads & Executes

// React hydration
ReactDOM.hydrate(
  <App />, 
  document.getElementById('root')
);

// Attaching event listeners
button.addEventListener('click', handleClick);
⚡ Hydrating...

3. Fully Interactive

Count: 0
✅ Interactive

⚠️ Hydration Challenges

Hydration Delay

Page appears interactive but doesn't respond to clicks until hydration completes

This button looks clickable but won't respond until "hydrated"
🔄

Hydration Mismatch

Server and client render different content, causing React to re-render

Server: "Welcome, Guest!"
Client: "Welcome, John!"
❌ Mismatch causes full re-render
📦

Bundle Size Impact

Large JavaScript bundles delay hydration and interactivity

Bundle Size: 350kb
Hydration Time: 1.2s

🚀 Hydration Optimization Techniques

📦 Selective Hydration

// Only hydrate interactive components
const InteractiveButton = lazy(() => 
  import('./InteractiveButton')
);

function Page() {
  return (
    <div>
      <h1>Static Content</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <InteractiveButton />
      </Suspense>
    </div>
  );
}

⚡ Progressive Hydration

// Hydrate components as they become visible
const useIntersectionObserver = (ref) => {
  const [isVisible, setIsVisible] = useState(false);
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
    });
    
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
  
  return isVisible;
};

🎮 Next.js SSR Live Demo

📱 Next.js Server-Side Rendering Example

This demo simulates how Next.js handles SSR with getServerSideProps, showing the complete request lifecycle.

1. Browser Request

GET /products/123
Accept: text/html
User-Agent: Browser
Waiting...

2. Server Processing

export async function getServerSideProps(context) {
  const { params } = context;
  const product = await fetchProduct(params.id);
  
  return {
    props: { product }
  };
}
Waiting...

3. HTML Generation

HTML will appear here...
Waiting...

4. Browser Receives & Hydrates

Rendered page will appear here...
Waiting...

📊 Request Metrics

Server Processing: -
HTML Generation: -
Total TTFB: -
Hydration: -

💻 Next.js SSR Code Examples

pages/products/[id].js

import { GetServerSideProps } from 'next';

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

interface Props {
  product: Product;
}

export default function ProductPage({ product }: Props) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>{product.description}</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { params } = context;
  
  // This runs on the server for every request
  const product = await fetch(`${process.env.API_URL}/products/${params.id}`)
    .then(res => res.json());
  
  if (!product) {
    return {
      notFound: true,
    };
  }
  
  return {
    props: {
      product,
    },
  };
};

pages/api/products/[id].js

import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query;
  
  if (req.method === 'GET') {
    try {
      // Simulate database fetch
      const product = await db.products.findUnique({
        where: { id: String(id) }
      });
      
      if (!product) {
        return res.status(404).json({ error: 'Product not found' });
      }
      
      // Set cache headers for better performance
      res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
      
      return res.status(200).json(product);
    } catch (error) {
      return res.status(500).json({ error: 'Internal server error' });
    }
  }
  
  return res.status(405).json({ error: 'Method not allowed' });
}

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable experimental features
  experimental: {
    // Enable React 18 features
    reactRoot: true,
    // Enable Concurrent Features
    concurrentFeatures: true,
  },
  
  // Configure image optimization
  images: {
    domains: ['example.com'],
    formats: ['image/webp', 'image/avif'],
  },
  
  // Configure headers for better performance
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=60, s-maxage=60',
          },
        ],
      },
    ];
  },
  
  // Configure redirects
  async redirects() {
    return [
      {
        source: '/old-products/:id',
        destination: '/products/:id',
        permanent: true,
      },
    ];
  },
  
  // Enable compression
  compress: true,
  
  // Configure build output
  output: 'standalone', // For Docker deployments
};

module.exports = nextConfig;

⚡ SSR Performance Optimization

🎯 Performance Bottlenecks in SSR

🐌

Slow Server Processing

Data fetching and HTML generation on every request

TTFB: 800-2000ms Server Load: High
📡

API Dependencies

Server must wait for external API calls to complete

API Latency: 200-500ms Waterfall Requests
💾

Database Queries

Complex queries and N+1 problems slow down rendering

Query Time: 100-800ms Connection Pool
🔄

Heavy Hydration

Large JavaScript bundles delay interactivity

Bundle: 300-800kb Hydration: 500-1500ms

🚀 Optimization Strategies

💾 Caching Strategies

Page-Level Caching
// Cache entire page for 60 seconds
export const getServerSideProps = async (context) => {
  context.res.setHeader(
    'Cache-Control',
    'public, s-maxage=60, stale-while-revalidate=300'
  );
  
  const data = await fetchData();
  return { props: { data } };
};
✅ Reduces server load ✅ Faster response times
Data-Level Caching
// Cache API responses with Redis
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);

export const getServerSideProps = async () => {
  const cacheKey = 'products:featured';
  let products = await redis.get(cacheKey);
  
  if (!products) {
    products = await fetchFeaturedProducts();
    await redis.setex(cacheKey, 300, JSON.stringify(products));
  } else {
    products = JSON.parse(products);
  }
  
  return { props: { products } };
};
✅ Reduces API calls ✅ Consistent performance

📡 Data Fetching Optimization

Parallel Data Fetching
export const getServerSideProps = async () => {
  // Fetch data in parallel instead of sequentially
  const [user, products, reviews] = await Promise.all([
    fetchUser(),
    fetchProducts(),
    fetchReviews()
  ]);
  
  return {
    props: { user, products, reviews }
  };
};
✅ Reduces total wait time ✅ Better resource utilization
GraphQL with DataLoader
// Batch and cache database queries
const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.findMany({
    where: { id: { in: userIds } }
  });
  return userIds.map(id => users.find(user => user.id === id));
});

// Usage in resolver
const user = await userLoader.load(userId);
✅ Eliminates N+1 queries ✅ Automatic batching

🎮 Performance Optimization Demo

Server Processing Time
-
Time to First Byte
-
First Contentful Paint
-
Total Page Load
-
📊 Optimization Impact
Run the performance test to see optimization benefits!

🎯 SSR Summary & Best Practices

📋 Key Takeaways

✅ SSR Strengths

  • Fast First Contentful Paint
  • Excellent SEO and social sharing
  • Better Core Web Vitals scores
  • Works without JavaScript
  • Fresh data on every request

❌ SSR Challenges

  • Higher server costs and complexity
  • Slower Time to Interactive
  • Hydration mismatches and delays
  • More complex deployment
  • API dependency bottlenecks

🏆 SSR Best Practices

🚀 Performance

💾
Implement Smart Caching

Use page-level, data-level, and CDN caching strategically

📡
Optimize Data Fetching

Fetch data in parallel, use GraphQL, implement connection pooling

Minimize Hydration Time

Code split, selective hydration, progressive enhancement

🛠️ Development

🔄
Handle Hydration Mismatches

Use suppressHydrationWarning sparingly, ensure server-client consistency

🚨
Implement Error Boundaries

Graceful error handling for server and client-side errors

📊
Monitor Performance

Track TTFB, hydration time, and Core Web Vitals

🎯 When to Choose SSR

✅ Perfect for:

  • E-commerce product pages
  • News and content websites
  • Marketing and landing pages
  • Social media platforms
  • SEO-critical applications

❌ Consider alternatives for:

  • Admin dashboards
  • Internal tools
  • Real-time applications
  • Apps with frequent updates
  • Cost-sensitive projects

🚀 Next: Explore Static Site Generation

Learn how SSG combines the best of SSR and CSR while eliminating server costs.

Continue to SSG →