Next.js offers powerful rendering strategies that go beyond traditional client-side React applications. Understanding Server-Side Rendering (SSR) and Static Site Generation (SSG) is crucial for building performant, SEO-friendly applications.
Core Rendering Strategies
1. Static Site Generation (SSG)
Pre-renders pages at build time – ideal for content that doesn’t change frequently.
Basic SSG with getStaticProps
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts }, // Will be passed to page component
revalidate: 60 // Optional: Enable ISR (re-generate every 60s)
};
}
function Blog({ posts }) {
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default Blog;
Dynamic Routes with SSG (getStaticPaths
)
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
const paths = posts.map(post => ({
params: { id: post.id.toString() }
}));
return { paths, fallback: false }; // or 'blocking' or true
}
export async function getStaticProps({ params }) {
const res = await fetch(`https://api.example.com/posts/${params.id}`);
const post = await res.json();
return { props: { post } };
}
function Post({ post }) {
return <article>{post.title}</article>;
}
export default Post;
2. Server-Side Rendering (SSR)
Renders pages on each request – ideal for personalized or frequently updated content.
Basic SSR with getServerSideProps
export async function getServerSideProps(context) {
// Context contains request-specific params
const { req, res, params, query } = context;
const userAgent = req.headers['user-agent'];
const res = await fetch(`https://api.example.com/data?user=${query.user}`);
const data = await res.json();
return {
props: { data, userAgent } // Passed to page component
};
}
function Page({ data, userAgent }) {
return (
<div>
<p>User Agent: {userAgent}</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default Page;
Advanced Patterns
1. Incremental Static Regeneration (ISR)
Update static content without rebuilding the entire site.
export async function getStaticProps() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
return {
props: { products },
revalidate: 3600 // Re-generate every hour if requests come in
};
}
2. Hybrid Rendering
Combine SSG, SSR, and client-side fetching in a single app.
// pages/index.js - SSG for homepage
export async function getStaticProps() { /* ... */ }
// pages/blog/[slug].js - SSG for blog posts
export async function getStaticPaths() { /* ... */ }
export async function getStaticProps() { /* ... */ }
// pages/dashboard.js - SSR for authenticated content
export async function getServerSideProps(context) { /* ... */ }
// pages/search.js - Client-side data fetching
function SearchPage() {
const [results, setResults] = useState([]);
const handleSearch = async (query) => {
const res = await fetch(`/api/search?q=${query}`);
setResults(await res.json());
};
return ( /* ... */ );
}
3. On-Demand Revalidation
Trigger static page updates via API routes.
// pages/api/revalidate.js
export default async function handler(req, res) {
if (req.query.secret !== process.env.REVALIDATE_TOKEN) {
return res.status(401).json({ message: 'Invalid token' });
}
try {
await res.revalidate('/path-to-revalidate');
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
// Then call from CMS webhook or manually:
// POST /api/revalidate?secret=<token>
Data Fetching Methods Comparison
Method | When to Use | Execution Time | Data Freshness |
---|---|---|---|
getStaticProps | Content known at build time | Build time | Stale until rebuild |
getStaticProps +ISR | Content that updates periodically | Build + runtime | Configurable refresh |
getServerSideProps | Personalized/private data | Each request | Always fresh |
Client-side fetching | User-specific, non-SEO critical data | Client navigation | Always fresh |
Performance Optimization
1. Static Assets Optimization
import Image from 'next/image';
function OptimizedImage() {
return (
<Image
src="/profile.jpg"
alt="Profile"
width={500}
height={500}
priority // For above-the-fold images
placeholder="blur" // Optional blur-up
blurDataURL="data:image/png;base64,..."
/>
);
}
2. Dynamic Imports (Code Splitting)
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(
() => import('../components/HeavyComponent'),
{
loading: () => <p>Loading...</p>,
ssr: false // Disable SSR for this component if needed
}
);
function Page() {
return <HeavyComponent />;
}
3. API Route Caching
// pages/api/data.js
export default async function handler(req, res) {
// Set cache headers
res.setHeader(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=300'
);
const data = await fetchData();
res.json(data);
}
TypeScript Integration
1. Typed Page Props
import { GetStaticProps, GetServerSideProps } from 'next';
interface Post {
id: number;
title: string;
content: string;
}
interface PageProps {
posts: Post[];
}
export const getStaticProps: GetStaticProps<PageProps> = async () => {
const res = await fetch('https://api.example.com/posts');
const posts: Post[] = await res.json();
return {
props: { posts }
};
};
function BlogPage({ posts }: PageProps) {
return ( /* ... */ );
}
export default BlogPage;
2. Typed API Routes
import type { NextApiRequest, NextApiResponse } from 'next';
type ResponseData = {
success: boolean;
data?: any;
error?: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' });
}
// Process request
res.status(200).json({ success: true, data: {} });
}
Best Practices
- Page Classification: Analyze each page’s needs to choose the right strategy
- ISR for Dynamic Content: Prefer over SSR when possible for better performance
- Client-side Complements: Use SWR or React Query for client-side data updates
- Layout Components: Use
_app.js
for shared layouts to avoid remounting - Error Handling: Implement proper error boundaries and status codes
- Security: Validate all API inputs and sanitize outputs
- Monitoring: Track cache hit ratios and SSR performance
Real-World Examples
1. E-commerce Site
// Homepage - SSG with ISR
export async function getStaticProps() {
const featured = await getFeaturedProducts();
return { props: { featured }, revalidate: 3600 };
}
// Product pages - SSG with dynamic paths
export async function getStaticPaths() {
const products = await getAllProductIds();
return { paths: products.map(id => ({ params: { id } })), fallback: 'blocking' };
}
// Cart page - SSR for user-specific data
export async function getServerSideProps({ req }) {
const cart = await getUserCart(req.cookies.userId);
return { props: { cart } };
}
2. Content Management System
// Blog index - SSG
export async function getStaticProps() {
const posts = await getAllPosts();
return { props: { posts } };
}
// Blog posts - SSG with on-demand revalidation
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug);
return { props: { post }, revalidate: 60 };
}
// Dashboard - SSR
export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { destination: '/login', permanent: false } };
}
return { props: { user: session.user } };
}
Understanding these Next.js rendering concepts allows you to build applications that are both performant and deliver excellent user experiences across different types of content and use cases.