DEV Community

Cover image for Understanding Next.js: SSR, CSR, ISR and SSG
Mike Varenek
Mike Varenek

Posted on

Understanding Next.js: SSR, CSR, ISR and SSG

Next.js offers flexibility in how your webpage content is rendered, catering to different performance and SEO needs. Here's a breakdown of the three main approaches:

1. Server-side Rendering (SSR):

What happens: The server renders the complete HTML for each page on every request.

Advantages:
SEO-friendly: Ready-to-index HTML boosts search engine visibility.
Faster initial load: Content instantly available as the user enters the page.
Dynamic content: Suitable for pages with constantly changing data.

Disadvantages:
Higher server load: Can be expensive for high-traffic sites.
Potentially slower updates: Each request requires server processing.

Example: A blog with real-time comment updates.

// pages/blog/[slug].js
export async function getServerSideProps(context) {
  const { slug } = context.params;
  const post = await fetchPostBySlug(slug);
  const comments = await fetchCommentsForPost(slug);

  return {
    props: {
      post,
      comments,
    },
  };
}

function BlogPost({ post, comments }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <ul>
        {comments.map((comment) => (
          <li key={comment.id}>{comment.content}</li>
        ))}
      </ul>
      <form onSubmit={handleCommentSubmit}>
        {/* ... */}
      </form>
    </div>
  );
}

export default BlogPost;
Enter fullscreen mode Exit fullscreen mode

This example fetches the post and comments on the server for each request, ensuring real-time updates but requiring more server resources.

2. Client-side Rendering (CSR):

What happens: The server sends a minimal HTML page, and the browser downloads and executes JavaScript to render the full content.

Advantages:
Highly interactive: Offers smooth user experience with dynamic updates.
Lower server load: Ideal for static content and user interactions.

Disadvantages:
SEO considerations: Search engines might struggle to index dynamic content.
Initial load delay: Content appears blank until JavaScript executes.

Example: A social media feed with dynamic user interactions.

// pages/feed.js
function Feed() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchPosts().then((data) => setPosts(data));
  }, []);

  return (
    <div>
      {posts.map((post) => (
        <Post key={post.id} {...post} />
      ))}
    </div>
  );
}

export default Feed;
Enter fullscreen mode Exit fullscreen mode

This example fetches posts using JavaScript on the client, enabling dynamic interactions like liking and commenting, but might have an initial delay and SEO challenges.

3. Static Site Generation (SSG):

What happens: HTML pages are pre-rendered with data at build time, creating static files served directly by the server.

Advantages:
Excellent SEO: Ready-to-index HTML with pre-fetched data.
Blazing-fast loading: Instant content delivery, no server rendering needed.
Scalability: Handles high traffic efficiently.

Disadvantages:
Limited dynamic content: Not ideal for frequently changing data.
Rebuilds required: Changes necessitate rebuilding and redeploying the entire site.
Example: A landing page with static content.

// pages/index.js
export async function getStaticProps() {
  const heroContent = await fetchHeroContent();

  return {
    props: {
      heroContent,
    },
    revalidate: 60 * 60, // Revalidate every hour
  };
}

function HomePage({ heroContent }) {
  return (
    <div>
      <h1>{heroContent.title}</h1>
      <p>{heroContent.description}</p>
      {/* ... */}
    </div>
  );
}

export default HomePage;

Enter fullscreen mode Exit fullscreen mode

revalidate: This property indicates how often (in seconds) the data for this page should be revalidated. In this case, it's set to revalidate every 3600 seconds (1 hour). During revalidation, Next.js will regenerate the page with the latest data.

This Next.js Static Site Generation example pre-renders the page at build time with fetched data, providing excellent SEO and performance but not ideal for frequently changing content.

4. Incremental Static Regeneration (ISR):

Next.js also offers Incremental Static Regeneration (ISR), where pre-rendered pages can be automatically refreshed with new data at specified intervals, combining the benefits of SSG and SSR.

Advantages:
Near-instant updates: Content is updated automatically in the background, ensuring users see the latest information without waiting for a full rebuild.
Improved performance: Static pages are served initially, leading to faster load times compared to full server-side rendering (SSR).
Reduced server load: Background updates minimize the number of server requests required, improving scalability and cost-efficiency.
Flexibility: ISR can be applied to specific pages based on their data update frequency, allowing for a mix of static and dynamic content within your application.
SEO benefits: Fresh content is readily available for search engines to crawl and index, potentially improving your search ranking.
Development efficiency: Simplifies development compared to fully custom API routes, as Next.js handles much of the data fetching and revalidation logic.

Disadvantages:
Data freshness trade-off: Data can be slightly stale between revalidation intervals, depending on your chosen timeframe.
Increased complexity: Implementing ISR adds an extra layer of complexity compared to static site generation (SSG), requiring more setup and configuration.
Potential for race conditions: Concurrent updates to the same data can lead to inconsistencies if not handled carefully.
Potentially higher costs: Frequent revalidations can increase server load and data transfer costs compared to pure SSG.

Example: An e-commerce product page with frequently updated inventory status.

pages/products/[slug].js:

import { useState, useEffect } from 'react';

export async function getStaticProps({ params }) {
  const { slug } = params;
  const product = await fetchProductBySlug(slug);

  const revalidate = process.env.NODE_ENV === 'production' ? 60 : 1; // Revalidate every minute in development

  return {
    props: {
      product,
      revalidate,
    },
  };
}

function ProductPage({ product, revalidate }) {
  const [isInCart, setIsInCart] = useState(false); // Track cart state
  const [cartCount, setCartCount] = useState(0); // Track cart item count

  useEffect(() => {
    // Check if already in cart on page load (optional)
    const isItemInCart = localStorage.getItem(`cart-${product.id}`);
    if (isItemInCart) {
      setIsInCart(true);
      setCartCount(parseInt(isItemInCart, 10)); // Update cart count if stored
    }
  }, []);

  async function handleAddToCart() {
    try {
      const response = await fetch("/api/cart", {
        method: "POST",
        body: JSON.stringify({ productId: product.id }),
      });

      if (!response.ok) {
        throw new Error("Failed to add to cart");
      }

      const data = await response.json(); 
      setIsInCart(true);
      setCartCount(data.cartCount || 1); // Update cart count from response

      if (typeof window.revalidate === "function") {
        window.revalidate();
      }

      localStorage.setItem(`cart-${product.id}`, cartCount);

    } catch (error) {
      console.error("Error adding to cart:", error);
    }
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>Inventory: {product.inventory}</p>
      <button onClick={handleAddToCart} disabled={isInCart}>
        {isInCart ? `In Cart (${cartCount})` : "Add to Cart"}
      </button>
    </div>
  );
}

export default ProductPage;

Enter fullscreen mode Exit fullscreen mode

api/cart.js:

import { updateProductInventory, addItemToCart } from '../../utils/db'; 

export default async function handler(req, res) {
  const { productId } = req.body;

  try {
    await updateProductInventory(productId);

    const cartItem = await addItemToCart(productId);

    res.status(200).json({ cartCount: cartItem.count }); // Respond with cart item count (optional)
  } catch (error) {
    console.error("Error adding to cart:", error);
    res.status(500).json({ error: "Failed to add to cart" });
  }
}

Enter fullscreen mode Exit fullscreen mode

This code implements updates, storing the item in local storage and updating the UI.
The cartCount state and API response handling are optional, depending on desired cart visibility and functionality.
The cart update triggers a request to the /api/cart API route to update the server-side inventory and cart data.
The handleAddToCart function waits for the server response before updating the UI to ensure data consistency.
The getStaticProps function fetches product data and sets a revalidate value to trigger automatic updates at specified intervals (60 seconds in production, 1 second in development)

Choosing the Right Method:

The optimal approach depends on your specific needs. Consider factors like:

SEO importance: Prioritize SSR or SSG for better search engine visibility.
Content dynamism: Choose SSR for constantly changing data, SSG for semi-static content, and CSR for highly interactive experiences.
Performance & Scalability: SSG excels in speed and scalability, while CSR and SSR trade performance for dynamic capabilities.

Resources
https://nextjs.org/docs/pages/building-your-application/rendering
https://spacema-dev.com/server-side-rendering-ssr-and-static-site-generation-ssg/
https://www.makeuseof.com/nextjs-rendering-methods-csr-ssr-ssg-isr/
https://hybridheroes.de/blog/2023-05-31-next-js-rendering-strategies/

Top comments (0)