Next.js course ยท Module 3: Styling and Visual Optimization
Performance Benchmarks in Next.js 16 - SSR vs SSG vs ISR
In this lesson7
In Quantum Metropolis, engineers constantly monitor and optimize the performance of all city systems - from energy grids to transportation networks. Every architectural decision impacts the speed and efficiency of the entire city. Similarly, in Next.js 16, choosing the right rendering strategy has a fundamental impact on application performance.
In this chapter, you will learn a detailed comparison of rendering strategies, performance metrics, and practical examples that will help you make informed architectural decisions.
Three Main Rendering Strategies in Next.js 16
1. SSR - Server-Side Rendering
How it works: Each user request causes the page to be rendered on the server in real-time.
// app/dashboard/page.tsx
// Since Next.js 15, fetch is not cached by default, but a page without request-time APIs
// would still be rendered at build time, so we explicitly force rendering on every request
export const dynamic = 'force-dynamic';
// OR
// fetch(..., { cache: 'no-store' })
async function getDashboardData() {
const res = await fetch('https://api.quantum.com/dashboard', {
cache: 'no-store' // Forces SSR
});
return res.json();
}
export default async function Dashboard() {
const data = await getDashboardData();
return (
<div>
<h1>Control panel</h1>
<p>Last update: {new Date().toLocaleTimeString()}</p>
<DashboardContent data={data} />
</div>
);
}Advantages:
- Always fresh data
- Excellent for personalized content
- Dynamic content without delays
Disadvantages:
- Slower TTFB (Time To First Byte)
- Higher server load
- Can be more expensive to host
2. SSG - Static Site Generation
How it works: Pages are generated once during build time and served as static HTML files.
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
cache: 'force-cache' // Forces SSG
});
return res.json();
}
export async function generateStaticParams() {
const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const post = await getPost((await params).slug);
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Advantages:
- Very fast TTFB
- Low hosting costs (CDN)
- Excellent for SEO
- Best performance
Disadvantages:
- Data may be outdated
- Long build time for large sites
- Requires rebuild for every change
3. ISR - Incremental Static Regeneration
How it works: Pages are generated statically but can be refreshed in the background after a specified time.
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.quantum.com/products/${id}`, {
next: { revalidate: 3600 } // Revalidation every hour
});
return res.json();
}
export async function generateStaticParams() {
// Generate only the most popular products at build time
const popularProducts = await fetch('https://api.quantum.com/products/popular')
.then(res => res.json());
return popularProducts.map((product) => ({
id: product.id.toString(),
}));
}
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const product = await getProduct((await params).id);
return (
<div>
<h1>{product.name}</h1>
<p>Price: {product.price} QC</p>
<p>Availability: {product.stock} units</p>
<button>Add to cart</button>
</div>
);
}
export const revalidate = 3600; // Global revalidation timeAdvantages:
- Compromise between SSR and SSG
- Fast TTFB like SSG
- Content refreshed automatically
- Pages generated on-demand
Disadvantages:
- First users may see stale data
- Requires configuring the revalidation time
- More complex cache logic
Performance Metrics Comparison
TTFB - Time To First Byte
TTFB is the time from sending a request to receiving the first byte of the response. In Metropolis Quantum, it's like the system's reaction time to a command - the faster, the better.
Benchmark (example values):
| Strategy | TTFB | Rating |
|---|---|---|
| SSG | 50-100ms | Excellent |
| ISR (cached) | 50-150ms | Very good |
| ISR (revalidating) | 200-500ms | Good |
| SSR | 300-800ms | Average |
| SSR (slow API) | 1000ms+ | Poor |
// Measuring TTFB in the browser
performance.getEntriesByType('navigation')[0].responseStart;LCP - Largest Contentful Paint
LCP measures the time until the largest content element is displayed. It's like the speed at which the main hologram appears in Metropolis Quantum.
Benchmark:
| Strategy | LCP | Rating |
|---|---|---|
| SSG | 0.5-1.5s | Excellent |
| ISR | 0.8-2s | Very good |
| SSR | 1.5-3s | Good |
| SSR (heavy) | 3s+ | Needs optimization |
Google recommendations:
- Good: < 2.5s
- Needs improvement: 2.5s - 4s
- Poor: > 4s
// Monitoring LCP
import { onLCP } from 'web-vitals';
onLCP((metric) => {
console.log('LCP:', metric.value);
// Send to analytics
analytics.track('Web Vitals', {
metric: 'LCP',
value: metric.value,
rating: metric.rating
});
});CLS - Cumulative Layout Shift
CLS measures the visual stability of a page. It's like the stability of holographic interfaces in Metropolis Quantum - they should not "jump".
Benchmark:
| Strategy | CLS | Rating |
|---|---|---|
| SSG | 0-0.05 | Excellent |
| ISR | 0-0.1 | Very good |
| SSR | 0.1-0.25 | Good (if well optimized) |
Google recommendations:
- Good: < 0.1
- Needs improvement: 0.1 - 0.25
- Poor: > 0.25
// Monitoring CLS
import { onCLS } from 'web-vitals';
onCLS((metric) => {
console.log('CLS:', metric.value);
analytics.track('Web Vitals', {
metric: 'CLS',
value: metric.value,
rating: metric.rating
});
});When to Use Which Strategy?
Choose SSG when:
Blog or marketing site
- Content changes rarely
- SEO is a priority
- You need the best performance
// app/blog/[slug]/page.tsx - Ideal for SSG
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
const post = await getPost((await params).slug);
return <Article post={post} />;
}Documentation site
- Static or rarely updated content
- Multilingual support
- Loading speed is critical
Landing pages
- Marketing campaigns
- Products/services
- High conversion = fast loading
Choose ISR when:
E-commerce (product pages)
- Data changes regularly (prices, stock)
- Large number of products
- Balance between freshness and performance
// app/products/[id]/page.tsx - Ideal for ISR
export const revalidate = 300; // 5 minutes
export default async function Product({ params }) {
const product = await getProduct((await params).id);
return (
<div>
<h1>{product.name}</h1>
<p>Price: {product.price}</p>
<p>In stock: {product.stock}</p>
</div>
);
}Newsroom / Media
- Articles updated several times a day
- Lots of content
- Need for speed + freshness
User profiles (public)
- Data changes rarely
- Large number of profiles
- Need for cache with refreshing
Choose SSR when:
Dashboard / Admin panel
- Real-time data
- Personalization
- User authorization
// app/dashboard/page.tsx - Ideal for SSR
export const dynamic = 'force-dynamic';
export default async function Dashboard() {
const session = await getServerSession();
const userData = await getUserData(session.userId);
return (
<div>
<h1>Welcome, {userData.name}</h1>
<RealTimeAnalytics userId={session.userId} />
</div>
);
}Search results
- Dynamic queries
- Personalized results
- Always fresh data
User-specific content
- Shopping cart
- User preferences
- Activity history
Real-world Examples with Benchmarks
Example 1: Tech Blog (SSG)
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost((await params).slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
};
}
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
async function getPost(slug: string) {
const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
cache: 'force-cache'
});
return res.json();
}
export default async function BlogPost({ params }) {
const post = await getPost((await params).slug);
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<Image
src={post.coverImage}
alt={post.title}
width={1200}
height={630}
preload
/>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Metrics:
- TTFB: ~80ms
- LCP: ~1.2s
- CLS: 0.02
- Build time: 2min for 500 posts
- Rating:
Example 2: E-commerce Marketplace (ISR)
// app/products/[id]/page.tsx
async function getProduct(id: string) {
const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
next: { revalidate: 600 } // Revalidation every 10 minutes
});
if (!res.ok) {
notFound();
}
return res.json();
}
export async function generateStaticParams() {
// Generate only top 100 products during build
const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
.then(res => res.json());
return topProducts.map((product) => ({
id: product.id.toString(),
}));
}
export default async function ProductPage({ params }) {
const product = await getProduct((await params).id);
return (
<div className="product-page">
<ProductGallery images={product.images} />
<div className="product-info">
<h1>{product.name}</h1>
<div className="price-stock">
<span className="price">{product.price} QC</span>
<StockIndicator stock={product.stock} />
</div>
<div className="description">
{product.description}
</div>
<AddToCartButton productId={product.id} />
<ProductReviews productId={product.id} />
</div>
<RelatedProducts categoryId={product.categoryId} />
</div>
);
}
export const revalidate = 600; // 10 minutesMetrics:
- TTFB: ~120ms (cached), ~400ms (revalidating)
- LCP: ~1.8s
- CLS: 0.05
- Build time: 5min for top 100 products
- On-demand generation: ~500ms for the rest
- Rating:
Example 3: Real-time Dashboard (SSR)
// app/dashboard/analytics/page.tsx
export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';
async function getRealTimeAnalytics(userId: string) {
const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
cache: 'no-store',
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`
}
});
return res.json();
}
export default async function AnalyticsDashboard() {
const session = await getServerSession();
if (!session) {
redirect('/login');
}
const analytics = await getRealTimeAnalytics(session.user.id);
return (
<div className="analytics-dashboard">
<h1>Real-time analytics</h1>
<div className="stats-grid">
<StatCard
title="Active Users"
value={analytics.activeUsers}
change={analytics.activeUsersChange}
/>
<StatCard
title="Revenue today"
value={`${analytics.todayRevenue} QC`}
change={analytics.revenueChange}
/>
<StatCard
title="Conversions"
value={`${analytics.conversionRate}%`}
change={analytics.conversionChange}
/>
</div>
<RevenueChart data={analytics.revenueChart} />
<UserActivityFeed data={analytics.recentActivity} />
</div>
);
}Metrics:
- TTFB: ~450ms
- LCP: ~2.4s
- CLS: 0.08
- Data freshness: Always current
- Rating: (good for real-time, but needs optimization)
Example 4: Hybrid Approach - Newsfeed (ISR + Client-side updates)
// app/news/page.tsx
async function getLatestNews() {
const res = await fetch('https://api.quantum-news.com/latest', {
next: { revalidate: 60 } // Revalidation every minute
});
return res.json();
}
export default async function NewsFeed() {
const initialNews = await getLatestNews();
return (
<div className="news-feed">
<h1>News from Metropolis Quantum</h1>
{/* Server-rendered initial content (ISR) */}
<NewsList initialNews={initialNews} />
{/* Client-side updates every 30 seconds */}
<ClientNewsUpdater />
</div>
);
}
export const revalidate = 60;// components/ClientNewsUpdater.tsx
'use client';
import { useEffect, useState } from 'react';
export function ClientNewsUpdater() {
const [latestNews, setLatestNews] = useState([]);
useEffect(() => {
const interval = setInterval(async () => {
const res = await fetch('/api/news/latest');
const data = await res.json();
setLatestNews(data);
}, 30000); // 30 seconds
return () => clearInterval(interval);
}, []);
if (latestNews.length === 0) return null;
return (
<div className="breaking-news">
<h2>Latest News</h2>
{latestNews.map(news => (
<NewsCard key={news.id} news={news} />
))}
</div>
);
}Metrics:
- Initial TTFB: ~150ms
- LCP: ~1.5s
- CLS: 0.04
- Freshness: ISR every minute + client updates every 30s
- Rating: (best of both worlds)
Performance Monitoring Tools
1. Web Vitals in Next.js 16
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/react';
export default function RootLayout({ children }) {
return (
<html lang="pl">
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
);
}2. Custom Web Vitals reporting
// app/web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
// Sending to your own analytics
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
});
fetch('/api/analytics/web-vitals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
});
return null;
}3. Lighthouse CI in the Pipeline
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on:
pull_request:
branches: [main]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli
lhci autorun
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: lighthouse-results
path: .lighthouseciOptimization for Each Strategy
SSG Optimizations
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
},
compress: true,
poweredByHeader: false,
generateEtags: true,
};ISR Optimizations
// Use on-demand revalidation for important updates
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
if (secret !== process.env.REVALIDATION_TOKEN) {
return Response.json({ message: 'Invalid token' }, { status: 401 });
}
const path = request.nextUrl.searchParams.get('path');
if (path) {
revalidatePath(path);
return Response.json({ revalidated: true });
}
return Response.json({ message: 'Missing path' }, { status: 400 });
}SSR Optimizations
// Use streaming for faster TTFB
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
);
}Summary and Decision Tree
Must data be real-time?
โโ YES โ SSR
โ โโ Consider streaming and Suspense for better performance
โ
โโ NO
โ
โโ Does data change more often than once per hour?
โ โโ YES โ ISR (revalidate: 60-3600)
โ โ โโ Consider on-demand revalidation for important updates
โ โ
โ โโ NO โ SSG
โ โโ Use generateStaticParams for all possible paths
โ
โโ Do you need personalization?
โโ YES โ SSR or ISR + Client Components
โโ NO โ SSG or ISRKey takeaways:
- SSG - fastest, but static (blogs, marketing, documentation)
- ISR - balance (e-commerce, news, public profiles)
- SSR - always fresh (dashboards, search, user content)
- Hybrid - often the best solution (ISR + Client-side updates)
Just as Quantum Metropolis engineers optimize every system for maximum performance, you too should consciously choose the rendering strategy for each part of your application. There is no single "best" solution - the key is matching the strategy to the specific use case.
Monitor Web Vitals, test different approaches, and always put user experience first. In the era of quantum technologies, every millisecond matters!
Code for this lesson: App.tsx
// CSS Modules - Creating scoped styles in Next.js
import React from 'react';
import styles from './Card.module.css';
// This component demonstrates CSS Modules in Next.js
// CSS classes are automatically unique (scoped)
export default function CSSModulesDemo() {
return (
<div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff' }}>
<h1 style={{ color: '#64ffda', marginBottom: '20px' }}>CSS Modules in Next.js</h1>
<div className={styles.card}>
<h2 className={styles.cardTitle}>Quantum Energy Monitor</h2>
<p className={styles.cardDescription}>
Monitoring energy levels in Sector Alpha
</p>
<span className={styles.badge}>Active</span>
</div>
<div className={styles.card}>
<h2 className={styles.cardTitle}>Transport Hub</h2>
<p className={styles.cardDescription}>
Status of the Metropolis transport network
</p>
<span className={styles.badge}>Online</span>
</div>
</div>
);
}Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which fetch option forces static generation (SSG) in Next.js 16 according to the lesson?
2. How did the lesson configure ISR (Incremental Static Regeneration)?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Build a complete design system for a Next.js application: 1. Configuration - create a folder structure for the design system 2. Design tokens - define colors, typography, spacings using CSS custom properties 3. Layout system - create a grid system with CSS Grid and Flexbox 4. Component library - build Button, Card, Modal components with variants 5. Theme system - implement light/dark mode with CSS variables 6. Responsive system - create breakpoints and utility classes 7. Animation system - add micro-interactions and transitions 8. Accessibility - implement focus states and ARIA attributes Use all the techniques you have learned: CSS Modules for components, Tailwind for utilities, CSS-in-JS for dynamic styles.