SSR, SSG, ISR & SPA โ Next.js Rendering Modes Explained with Real Examples


In the world of modern web development, choosing the right rendering strategy can significantly impact your application's performance, SEO, and user experience. Next.js, a popular React framework, offers multiple rendering modes to help developers optimize their applications for different use cases.
In this article, we'll dive deep into the four primary rendering strategies in Next.js: Server-Side Rendering (SSR), Static Site Generation (SSG), Incremental Static Regeneration (ISR), and Single-Page Applications (SPA). We'll explain each with real-world examples and practical code snippets to help you understand when and how to use each approach.
Understanding Rendering in Next.js
Before we dive into the specific rendering modes, let's clarify what "rendering" means in the context of web applications.
Rendering is the process of generating HTML from your React components. This can happen at different times:
Build time: HTML is generated when you build your application
Request time: HTML is generated when a user requests a page
Client-side: HTML is generated in the browser using JavaScript
Next.js gives developers the flexibility to choose when and where rendering happens for each page or component in their application. Let's explore each option in detail.
Server-Side Rendering (SSR)
SSR generates HTML for each request on the server; this means that every time a user visits a page, the server computes the HTML specifically for that request.
When to use SSR:
Pages that need access to request-specific data (like cookies or headers)
Pages with frequently changing data
Pages that require user-specific content
When SEO is important and content changes often
Real-world example: E-commerce Product Page
An e-commerce product page is a perfect candidate for SSR because:
Product availability and pricing might change frequently
You want to show personalized recommendations
SEO is critical for product discoverability
Code example:
// pages/products/[id].js
export default function ProductPage({ product, recommendations }) {
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<div className="stock-status">
{product.inStock ? 'In Stock' : 'Out of Stock'}
</div>
<h2>You might also like</h2>
<div className="recommendations">
{recommendations.map(item => (
<ProductCard key={item.id} product={item} />
))}
</div>
</div>
);
}
// This function runs on every request
export async function getServerSideProps(context) {
const { id } = context.params;
const { req } = context;
// Get user's cookie for personalization
const userToken = req.cookies.userToken;
// Fetch the latest product data
const product = await fetchProduct(id);
// Get personalized recommendations based on user history
const recommendations = await fetchRecommendations(id, userToken);
return {
props: {
product,
recommendations,
},
};
}
Performance characteristics:
Time to First Byte (TTFB): Slower than static methods because the server needs to generate HTML for each request
SEO: Excellent, as search engines receive fully rendered HTML
Data freshness: Always up-to-date
Server load: Higher, as the server processes each request
Static Site Generation (SSG)
Static Site Generation pre-renders pages at build time. The resulting HTML is stored on a CDN and reused for each request, making page loads extremely fast.
When to use SSG:
Marketing pages, blog posts, documentation
Content that doesn't change frequently
Pages that don't need to be personalized
When maximum performance is a priority
Real-world example: Blog Post
A blog post is an ideal candidate for SSG because:
Content rarely changes after publication
SEO is crucial for discoverability
Fast loading time improves user experience
Code example:
// pages/blog/[slug].js
export default function BlogPost({ post, author }) {
return (
<article>
<h1>{post.title}</h1>
<div className="author-info">
<img src={author.avatar} alt={author.name} />
<span>By {author.name}</span>
</div>
<div className="post-date">{new Date(post.date).toLocaleDateString()}</div>
<div className="content" dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// This function runs at build time
export async function getStaticProps({ params }) {
const { slug } = params;
// Fetch blog post data
const post = await fetchBlogPost(slug);
// Fetch author data
const author = await fetchAuthor(post.authorId);
return {
props: {
post,
author,
},
};
}
// This function determines which paths to pre-render
export async function getStaticPaths() {
// Fetch list of all blog posts
const posts = await fetchAllBlogPosts();
// Generate paths for each post
const paths = posts.map(post => ({
params: { slug: post.slug },
}));
return {
paths,
// fallback: false means 404 for any paths not returned by getStaticPaths
fallback: false,
};
}
Performance characteristics:
TTFB: Very fast, as HTML is pre-generated and served from CDN
SEO: Excellent, search engines receive complete HTML
Data freshness: Data can be stale if content changes after build
Build time: Increases with the number of pages
Server load: Minimal during user requests, as pages are pre-rendered
Incremental Static Regeneration (ISR)
ISR combines the benefits of SSG and SSR. It allows you to create or update static pages after you've built your site. This means you get the performance benefits of static generation while still showing fresh content.
When to use ISR:
Content that changes occasionally but not on every request
Pages with high traffic where performance is critical
E-commerce category pages, news sites, etc.
Real-world example: News Article Page
News articles are perfect for ISR because:
Content doesn't change after publication, but new comments might be added
Articles receive most traffic shortly after publication
You want the performance benefits of static generation
Code example:
// pages/news/[slug].js
export default function NewsArticle({ article, comments }) {
return (
<div>
<h1>{article.headline}</h1>
<div className="byline">By {article.author}</div>
<div className="published-date">
{new Date(article.publishedAt).toLocaleString()}
</div>
<div className="content" dangerouslySetInnerHTML={{ __html: article.content }} />
<section className="comments">
<h2>Comments ({comments.length})</h2>
{comments.map(comment => (
<div key={comment.id} className="comment">
<strong>{comment.username}</strong>
<p>{comment.text}</p>
</div>
))}
</section>
</div>
);
}
// This runs at build time and then re-runs at most every 10 minutes
export async function getStaticProps({ params }) {
const { slug } = params;
// Fetch article data
const article = await fetchArticle(slug);
// Fetch comments
const comments = await fetchComments(article.id);
return {
props: {
article,
comments,
},
// Re-generate page at most once every 10 minutes
revalidate: 600,
};
}
export async function getStaticPaths() {
// Only pre-render the most important articles at build time
const featuredArticles = await fetchFeaturedArticles();
const paths = featuredArticles.map(article => ({
params: { slug: article.slug },
}));
return {
paths,
// fallback: 'blocking' means ungenerated pages will be rendered on-demand (like SSR)
// and then cached for subsequent requests
fallback: 'blocking',
};
}
Performance characteristics:
TTFB: Fast for cached pages, similar to SSR for the first request on uncached pages
SEO: Excellent, search engines receive complete HTML
Data freshness: Periodically updated based on the revalidate interval
Server load: Moderate, as pages are regenerated periodically, not on every request
Single-Page Application (SPA)
In the SPA model, rendering happens entirely on the client side. The initial HTML is minimal, and JavaScript takes over to render the UI in the browser.
Next.js supports this approach through client-side data fetching with SWR or React Query or by using the new App Router with the 'use client'
directive.
When to use SPA:
Highly interactive applications
Private, authenticated pages where SEO isn't a concern
Dashboard interfaces with real-time updates
When you want to minimize server load
Real-world example: User Dashboard
A user dashboard is ideal for client-side rendering because
It's behind authentication, so SEO isn't needed
It often requires real-time updates
Interactive elements benefit from client-side state management
Code example (using App Router):
'use client'
import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
export default function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
// Client-side data fetching
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['dashboard-stats'],
queryFn: async () => {
const res = await fetch('/api/dashboard/stats');
if (!res.ok) throw new Error('Failed to fetch stats');
return res.json();
}
});
const { data: activities, isLoading: activitiesLoading } = useQuery({
queryKey: ['activities'],
queryFn: async () => {
const res = await fetch('/api/dashboard/activities');
if (!res.ok) throw new Error('Failed to fetch activities');
return res.json();
}
});
if (statsLoading || activitiesLoading) {
return <div className="dashboard-loading">Loading dashboard data...</div>;
}
return (
<div className="dashboard">
<nav className="dashboard-tabs">
<button
className={activeTab === 'overview' ? 'active' : ''}
onClick={() => setActiveTab('overview')}
>
Overview
</button>
<button
className={activeTab === 'activities' ? 'active' : ''}
onClick={() => setActiveTab('activities')}
>
Activities
</button>
<button
className={activeTab === 'settings' ? 'active' : ''}
onClick={() => setActiveTab('settings')}
>
Settings
</button>
</nav>
{activeTab === 'overview' && (
<div className="dashboard-overview">
<div className="stats-grid">
<div className="stat-card">
<h3>Total Users</h3>
<div className="stat-value">{stats.totalUsers.toLocaleString()}</div>
</div>
<div className="stat-card">
<h3>Active Today</h3>
<div className="stat-value">{stats.activeToday.toLocaleString()}</div>
</div>
{/* More stat cards */}
</div>
</div>
)}
{activeTab === 'activities' && (
<div className="activities-list">
{activities.map(activity => (
<div key={activity.id} className="activity-item">
<div className="activity-icon">{activity.type === 'comment' ? '๐ฌ' : '๐'}</div>
<div className="activity-details">
<strong>{activity.user}</strong> {activity.action}
<div className="activity-time">{new Date(activity.timestamp).toLocaleString()}</div>
</div>
</div>
))}
</div>
)}
{activeTab === 'settings' && (
<div className="settings-panel">
<h2>Dashboard Settings</h2>
{/* Settings form */}
</div>
)}
</div>
);
}
Performance characteristics:
TTFB: Fast, but users see a loading state until data is fetched
SEO: Poor, as search engines might not execute JavaScript
Data freshness: Always up-to-date as data is fetched on the client
Server load: Low for HTML serving, but API endpoints might receive heavy traffic
Interactivity: Excellent, as the application runs in the browser
Choosing the Right Rendering Strategy
To help you decide which rendering strategy to use, consider these factors:
SEO requirements: If SEO is critical, prefer SSR, SSG, or ISR over client-side rendering
Data freshness: For real-time data, use SSR or client-side rendering
Performance needs: For maximum performance, use SSG or ISR
Build time constraints: If you have many pages, SSG might lead to long build times
Interactivity: Highly interactive pages benefit from client-side rendering
User experience: Consider the loading experience for each approach
Here's a quick reference table:
Rendering Mode | SEO | Performance | Data Freshness | Build Time | Best For |
SSR | Excellent | Good | Excellent | N/A | Personalized content, frequently updated data |
SSG | Excellent | Excellent | Static | Increases with pages | Marketing sites, blogs, documentation |
ISR | Excellent | Excellent | Good | Moderate | E-commerce, news sites with occasional updates |
SPA | Poor | Variable | Excellent | Fast | Dashboards, authenticated applications |
Mixing Strategies for Optimal Performance
Next.js allows you to use different rendering strategies for different parts of your application. This hybrid approach lets you optimize each page according to its specific requirements.
Example hybrid approach:
Homepage: SSG for fast loading
Product listing: ISR to balance performance and freshness
Product detail: SSR for real-time inventory and pricing
User dashboard: Client-side rendering for interactivity
Conclusion
Next.js offers unparalleled flexibility in how you render your applications. By understanding the tradeoffs between SSR, SSG, ISR, and SPA rendering, you can make informed decisions that optimize for performance, SEO, and user experience.
Remember that there's no one-size-fits-all solution. The best approach often involves mixing rendering strategies based on the specific requirements of each page or component.
As you build your Next.js applications, continually evaluate your rendering choices and measure their impact on real-world performance metrics like Core Web Vitals. This data-driven approach will help you refine your rendering strategy over time.
Happy coding!
Subscribe to my newsletter
Read articles from Timothy George Benjamindas directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Timothy George Benjamindas
Timothy George Benjamindas
I am a freelance web developer. I provide easier solution for your SaaS ideas and cms with personal website. I use Next.js primarily and the PERN stack based on the client requirements. I am here to find a good place to share and grow my knowledge as well as good projects.