Menu

React & Next.js Topic

Next.js Rendering Strategies

Overview of Rendering Strategies

Strategy Data Fetching When HTML is Generated SEO Performance Use Cases
SSR Server on request Each request Excellent Good (slower initial) Dashboard, E-commerce, News
SSG Build time Build time Excellent Excellent (fastest) Blog, Documentation, Marketing
CSR Client Client Poor Good (slower FCP) Web Apps, Tools, Dashboards
ISR Build + Runtime Hybrid Excellent Excellent E-commerce, News, Content

1. SSR (Server-Side Rendering)

What is SSR?

  • HTML is generated on the server for each request

  • Data is fetched on the server before sending to client

  • Full HTML is sent to browser, then hydrated on client

When to Use SSR:

  • Dynamic data that changes frequently

  • User-specific content (dashboards, user profiles)

  • SEO-critical pages with dynamic content

  • Real-time data that can't be cached

  • Authentication-required pages

Next.js SSR Example:

jsx

// pages/dashboard.js - Using getServerSideProps
export default function Dashboard({ user, recentActivity, serverTime }) {
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <p>Server rendered at: {serverTime}</p>
      
      <div className="recent-activity">
        <h2>Recent Activity</h2>
        {recentActivity.map(activity => (
          <div key={activity.id} className="activity-item">
            <span>{activity.action}</span>
            <time>{activity.timestamp}</time>
          </div>
        ))}
      </div>
    </div>
  );
}

// This runs on EVERY request
export async function getServerSideProps(context) {
  const { req, res, query, params } = context;
  
  // Access request data (cookies, headers, etc.)
  const token = req.cookies.authToken;
  
  if (!token) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    };
  }

  try {
    // Fetch user-specific data
    const [userResponse, activityResponse] = await Promise.all([
      fetch(`https://api.example.com/user`, {
        headers: { Authorization: `Bearer ${token}` }
      }),
      fetch(`https://api.example.com/activity/recent`, {
        headers: { Authorization: `Bearer ${token}` }
      })
    ]);

    const user = await userResponse.json();
    const recentActivity = await activityResponse.json();

    // Data is fresh for every request
    return {
      props: {
        user,
        recentActivity,
        serverTime: new Date().toISOString()
      },
    };
  } catch (error) {
    // Handle errors gracefully
    return {
      redirect: {
        destination: '/error',
        permanent: false,
      },
    };
  }
}

React-Only SSR Example (without Next.js):

jsx

// server/index.js (Express.js)
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from '../src/App';

const app = express();

app.get('*', async (req, res) => {
  try {
    // Fetch data on server
    const userData = await fetchUserData(req.cookies.userId);
    const posts = await fetchRecentPosts();
    
    // Render React to HTML string
    const html = renderToString(
      <App userData={userData} posts={posts} />
    );
    
    // Send complete HTML response
    res.send(`
      <!DOCTYPE html>
      <html>
        <head>
          <title>SSR Example</title>
        </head>
        <body>
          <div id="root">${html}</div>
          <script>
            // Pass data to client for hydration
            window.__INITIAL_DATA__ = ${JSON.stringify({ userData, posts })};
          </script>
          <script src="/client-bundle.js"></script>
        </body>
      </html>
    `);
  } catch (error) {
    res.status(500).send('Server Error');
  }
});

2. SSG (Static Site Generation)

What is SSG?

  • HTML is generated at build time

  • Same HTML is served for all users

  • Can be deployed to CDN for maximum performance

  • Data is fetched during build process

When to Use SSG:

  • Content that doesn't change often (blog posts, documentation)

  • Marketing pages (homepage, about, contact)

  • Product catalogs that update infrequently

  • SEO-focused content that needs fast loading

  • High-traffic pages where performance is critical

Next.js SSG Example:

jsx

// pages/blog/[slug].js - Static Generation with dynamic routes
export default function BlogPost({ post, relatedPosts }) {
  if (!post) {
    return <div>Post not found</div>;
  }

  return (
    <article>
      <header>
        <h1>{post.title}</h1>
        <p>By {post.author} • {new Date(post.publishedAt).toLocaleDateString()}</p>
      </header>
      
      <div className="content" dangerouslySetInnerHTML={{ __html: post.content }} />
      
      <aside className="related-posts">
        <h3>Related Posts</h3>
        {relatedPosts.map(related => (
          <div key={related.id}>
            <a href={`/blog/${related.slug}`}>{related.title}</a>
          </div>
        ))}
      </aside>
    </article>
  );
}

// Define which paths to pre-render at build time
export async function getStaticPaths() {
  // Fetch all blog post slugs
  const posts = await fetch('https://api.example.com/posts').then(res => res.json());
  
  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }));

  return {
    paths,
    fallback: false // Show 404 for paths not generated at build time
    // fallback: true - Generate on demand
    // fallback: 'blocking' - Generate on demand, wait for generation
  };
}

// Fetch data at build time
export async function getStaticProps({ params }) {
  try {
    const [postResponse, relatedResponse] = await Promise.all([
      fetch(`https://api.example.com/posts/${params.slug}`),
      fetch(`https://api.example.com/posts/${params.slug}/related`)
    ]);

    const post = await postResponse.json();
    const relatedPosts = await relatedResponse.json();

    if (!post) {
      return {
        notFound: true // Show 404 page
      };
    }

    return {
      props: {
        post,
        relatedPosts
      },
      // Re-generate page in background after 1 hour
      revalidate: 3600 // ISR: Incremental Static Regeneration
    };
  } catch (error) {
    return {
      notFound: true
    };
  }
}

Static Generation for Multiple Pages:

jsx

// pages/index.js - Homepage
export default function Home({ featuredPosts, stats }) {
  return (
    <div>
      <h1>Welcome to Our Blog</h1>
      <div className="stats">
        <span>{stats.totalPosts} Posts</span>
        <span>{stats.totalAuthors} Authors</span>
      </div>
      
      <section className="featured-posts">
        <h2>Featured Posts</h2>
        {featuredPosts.map(post => (
          <article key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.excerpt}</p>
            <a href={`/blog/${post.slug}`}>Read More</a>
          </article>
        ))}
      </section>
    </div>
  );
}

// This runs only at build time
export async function getStaticProps() {
  const [featuredResponse, statsResponse] = await Promise.all([
    fetch('https://api.example.com/posts/featured'),
    fetch('https://api.example.com/stats')
  ]);

  const featuredPosts = await featuredResponse.json();
  const stats = await statsResponse.json();

  return {
    props: {
      featuredPosts,
      stats
    },
    revalidate: 3600 // Revalidate every hour
  };
}

3. CSR (Client-Side Rendering)

What is CSR?

  • HTML is generated in the browser using JavaScript

  • Initial page load is minimal, then content is populated

  • Data is fetched on the client after component mounts

  • Better for interactive applications

When to Use CSR:

  • Highly interactive applications (dashboards, admin panels)

  • User-specific content that doesn't need SEO

  • Real-time applications (chat, notifications)

  • When you need rich client-side interactions

  • Pages behind authentication

React CSR Example:

jsx

import { useState, useEffect } from 'react';
import useSWR from 'swr';

// Custom hook for data fetching
function useUserProfile(userId) {
  const { data, error, isLoading, mutate } = useSWR(
    userId ? `/api/users/${userId}` : null,
    url => fetch(url).then(res => res.json()),
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true
    }
  );

  return {
    user: data,
    isLoading,
    error,
    refresh: mutate
  };
}

function UserDashboard() {
  const [userId, setUserId] = useState(null);
  const [activeTab, setActiveTab] = useState('profile');
  
  const { user, isLoading, error, refresh } = useUserProfile(userId);

  // Simulate user authentication
  useEffect(() => {
    const storedUserId = localStorage.getItem('userId');
    setUserId(storedUserId);
  }, []);

  if (!userId) {
    return <div>Please log in to view your dashboard.</div>;
  }

  if (error) {
    return (
      <div>
        <p>Error loading profile: {error.message}</p>
        <button onClick={refresh}>Retry</button>
      </div>
    );
  }

  if (isLoading) {
    return (
      <div className="loading">
        <div className="spinner"></div>
        <p>Loading your dashboard...</p>
      </div>
    );
  }

  return (
    <div className="dashboard">
      <header>
        <h1>Welcome back, {user?.name}!</h1>
        <button onClick={refresh}>Refresh Data</button>
      </header>

      <nav className="tabs">
        <button 
          className={activeTab === 'profile' ? 'active' : ''}
          onClick={() => setActiveTab('profile')}
        >
          Profile
        </button>
        <button 
          className={activeTab === 'settings' ? 'active' : ''}
          onClick={() => setActiveTab('settings')}
        >
          Settings
        </button>
        <button 
          className={activeTab === 'analytics' ? 'active' : ''}
          onClick={() => setActiveTab('analytics')}
        >
          Analytics
        </button>
      </nav>

      <div className="tab-content">
        {activeTab === 'profile' && <UserProfile user={user} />}
        {activeTab === 'settings' && <UserSettings user={user} />}
        {activeTab === 'analytics' && <UserAnalytics userId={userId} />}
      </div>
    </div>
  );
}

// Child components that fetch their own data
function UserAnalytics({ userId }) {
  const [timeRange, setTimeRange] = useState('7d');
  const { data: analytics, isLoading } = useSWR(
    `/api/analytics/${userId}?range=${timeRange}`,
    url => fetch(url).then(res => res.json())
  );

  if (isLoading) return <div>Loading analytics...</div>;

  return (
    <div>
      <select value={timeRange} onChange={(e) => setTimeRange(e.target.value)}>
        <option value="24h">24 Hours</option>
        <option value="7d">7 Days</option>
        <option value="30d">30 Days</option>
      </select>
      
      <div className="analytics-data">
        <div>Page Views: {analytics?.pageViews}</div>
        <div>Visitors: {analytics?.uniqueVisitors}</div>
        <div>Bounce Rate: {analytics?.bounceRate}%</div>
      </div>
    </div>
  );
}

Next.js Hybrid Approach (SSG + CSR):

jsx

// pages/products/index.js - SSG for initial render, CSR for interactions
export default function ProductsPage({ initialProducts, categories }) {
  const [selectedCategory, setSelectedCategory] = useState('all');
  const [sortBy, setSortBy] = useState('name');
  const [products, setProducts] = useState(initialProducts);
  const [loading, setLoading] = useState(false);

  // Client-side filtering and sorting
  useEffect(() => {
    if (selectedCategory === 'all' && sortBy === 'name') {
      setProducts(initialProducts);
      return;
    }

    setLoading(true);
    
    // Simulate API call for filtered/sorted data
    const fetchFilteredProducts = async () => {
      const query = new URLSearchParams({
        category: selectedCategory,
        sort: sortBy
      });
      
      const response = await fetch(`/api/products?${query}`);
      const filteredProducts = await response.json();
      setProducts(filteredProducts);
      setLoading(false);
    };

    fetchFilteredProducts();
  }, [selectedCategory, sortBy, initialProducts]);

  return (
    <div>
      <h1>Our Products</h1>
      
      {/* Filters - client side interaction */}
      <div className="filters">
        <select 
          value={selectedCategory} 
          onChange={(e) => setSelectedCategory(e.target.value)}
        >
          <option value="all">All Categories</option>
          {categories.map(category => (
            <option key={category.id} value={category.slug}>
              {category.name}
            </option>
          ))}
        </select>

        <select 
          value={sortBy} 
          onChange={(e) => setSortBy(e.target.value)}
        >
          <option value="name">Name</option>
          <option value="price">Price</option>
          <option value="rating">Rating</option>
        </select>
      </div>

      {/* Products list */}
      {loading ? (
        <div>Loading products...</div>
      ) : (
        <div className="products-grid">
          {products.map(product => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      )}
    </div>
  );
}

// Static generation for initial data
export async function getStaticProps() {
  const [productsResponse, categoriesResponse] = await Promise.all([
    fetch('https://api.example.com/products'),
    fetch('https://api.example.com/categories')
  ]);

  const initialProducts = await productsResponse.json();
  const categories = await categoriesResponse.json();

  return {
    props: {
      initialProducts,
      categories
    },
    revalidate: 3600 // Revalidate every hour
  };
}

4. When to Use Which Strategy - Decision Guide

Decision Flowchart:

text

Is the content dynamic per user?
├── Yes → Does it need SEO?
│   ├── Yes → Use SSR (getServerSideProps)
│   └── No → Use CSR
└── No → Does content change frequently?
    ├── Yes → Use ISR (revalidate)
    └── No → Use SSG (getStaticProps)

Detailed Use Case Analysis:

jsx

// USE CASE 1: E-commerce Product Page
// Mix of SSG for product data + CSR for user interactions
export default function ProductPage({ product }) {
  // SSG: Product details, reviews, specifications
  // CSR: Add to cart, wishlist, real-time stock, recommendations
  
  return (
    <div>
      {/* SSG Content */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* CSR Interactions */}
      <AddToCart productId={product.id} />
      <WishlistButton productId={product.id} />
      <RealTimeStock productId={product.id} />
    </div>
  );
}

// USE CASE 2: Blog Platform
// SSG for posts + ISR for updates
export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
      {/* Static comments loaded via CSR */}
      <CommentsSection postId={post.id} />
    </article>
  );
}

// USE CASE 3: User Dashboard
// SSR for initial data + CSR for real-time updates
export default function Dashboard({ initialData }) {
  const [realTimeData, setRealTimeData] = useState(initialData);
  
  useEffect(() => {
    // WebSocket or polling for real-time updates
    const ws = new WebSocket('/api/realtime');
    ws.onmessage = (event) => {
      setRealTimeData(JSON.parse(event.data));
    };
    
    return () => ws.close();
  }, []);

  return (
    <div>
      <h1>Dashboard</h1>
      <RealTimeMetrics data={realTimeData} />
    </div>
  );
}

Performance Comparison:

jsx

// Performance metrics for each strategy
const performanceComparison = {
  SSR: {
    timeToFirstByte: 'Medium',
    firstContentfulPaint: 'Fast',
    timeToInteractive: 'Medium',
    seo: 'Excellent',
    cache: 'Poor'
  },
  SSG: {
    timeToFirstByte: 'Very Fast',
    firstContentfulPaint: 'Very Fast',
    timeToInteractive: 'Fast',
    seo: 'Excellent',
    cache: 'Excellent'
  },
  CSR: {
    timeToFirstByte: 'Fast',
    firstContentfulPaint: 'Slow',
    timeToInteractive: 'Medium',
    seo: 'Poor',
    cache: 'Good'
  },
  ISR: {
    timeToFirstByte: 'Very Fast',
    firstContentfulPaint: 'Very Fast',
    timeToInteractive: 'Fast',
    seo: 'Excellent',
    cache: 'Excellent'
  }
};

Summary:

  1. Use SSG whenever possible for maximum performance

  2. Use SSR for user-specific, SEO-critical dynamic content

  3. Use CSR for highly interactive, private content

  4. Combine strategies for optimal results

  5. Implement ISR for content that updates periodically

  6. Monitor real user metrics to validate your choices

This comprehensive guide helps you choose the right rendering strategy based on your specific use case, balancing performance, SEO, and user experience requirements.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?