Next.js course ยท Module 3: Styling and Visual Optimization

Performance Benchmarks in Next.js 16 - SSR vs SSG vs ISR

12 min read ยท
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 time

Advantages:

  • 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):

StrategyTTFBRating
SSG50-100msExcellent
ISR (cached)50-150msVery good
ISR (revalidating)200-500msGood
SSR300-800msAverage
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:

StrategyLCPRating
SSG0.5-1.5sExcellent
ISR0.8-2sVery good
SSR1.5-3sGood
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:

StrategyCLSRating
SSG0-0.05Excellent
ISR0-0.1Very good
SSR0.1-0.25Good (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 minutes

Metrics:

  • 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: .lighthouseci

Optimization 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 ISR

Key takeaways:

  1. SSG - fastest, but static (blogs, marketing, documentation)
  2. ISR - balance (e-commerce, news, public profiles)
  3. SSR - always fresh (dashboards, search, user content)
  4. 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. 1. Which fetch option forces static generation (SSG) in Next.js 16 according to the lesson?

  2. 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.

Useful articles