We use cookies to enhance your experience on the site
CodeWorlds

Performance Benchmarks w Next.js 15 - SSR vs SSG vs ISR

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 15, 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 15

1. SSR - Server-Side Rendering (Renderowanie po stronie serwera)

How it works: Each user request causes the page to be rendered on the server in real-time.

1// app/dashboard/page.tsx
2// By default Next.js 15 uses cache, so we need to explicitly disable it
3export const dynamic = 'force-dynamic';
4// LUB
5// fetch(..., { cache: 'no-store' })
6
7async function getDashboardData() {
8  const res = await fetch('https://api.quantum.com/dashboard', {
9    cache: 'no-store' // Wymusza SSR
10  });
11  return res.json();
12}
13
14export default async function Dashboard() {
15  const data = await getDashboardData();
16
17  return (
18    <div>
19      <h1>Panel kontrolny</h1>
20      <p>Ostatnia aktualizacja: {new Date().toLocaleTimeString()}</p>
21      <DashboardContent data={data} />
22    </div>
23  );
24}

Advantages:

  • ✅ Always fresh data
  • ✅ Excellent for personalized content
  • ✅ Dynamic content without delays

Disadvantages:

  • ❌ Wolniejszy TTFB (Time To First Byte)
  • ❌ Higher server load
  • ❌ Can be more expensive to host

2. SSG - Static Site Generation (Generowanie statyczne)

How it works: Pages are generated once during build time and served as static HTML files.

1// app/blog/[slug]/page.tsx
2
3async function getPost(slug: string) {
4  const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
5    cache: 'force-cache' // Wymusza SSG
6  });
7  return res.json();
8}
9
10export async function generateStaticParams() {
11  const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());
12
13  return posts.map((post) => ({
14    slug: post.slug,
15  }));
16}
17
18export default async function BlogPost({
19  params,
20}: {
21  params: Promise<{ slug: string }>;
22}) {
23  const post = await getPost((await params).slug);
24
25  return (
26    <article>
27      <h1>{post.title}</h1>
28      <time>{post.publishedAt}</time>
29      <div dangerouslySetInnerHTML={{ __html: post.content }} />
30    </article>
31  );
32}

Advantages:

  • ✅ Bardzo szybki TTFB
  • ✅ Niskie koszty hostingu (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.

1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4  const res = await fetch(`https://api.quantum.com/products/${id}`, {
5    next: { revalidate: 3600 } // Revalidation every hour
6  });
7  return res.json();
8}
9
10export async function generateStaticParams() {
11  // Generuj tylko najpopularniejsze produkty podczas build
12  const popularProducts = await fetch('https://api.quantum.com/products/popular')
13    .then(res => res.json());
14
15  return popularProducts.map((product) => ({
16    id: product.id.toString(),
17  }));
18}
19
20export default async function ProductPage({
21  params,
22}: {
23  params: Promise<{ id: string }>;
24}) {
25  const product = await getProduct((await params).id);
26
27  return (
28    <div>
29      <h1>{product.name}</h1>
30      <p>Cena: {product.price} QC</p>
31      <p>Availability: {product.stock} units</p>
32      <button>Dodaj do koszyka</button>
33    </div>
34  );
35}
36
37export const revalidate = 3600; // Globalny czas rewalidacji

Advantages:

  • ✅ Compromise between SSR and SSG
  • ✅ Szybki TTFB jak SSG
  • ✅ Content refreshed automatically
  • ✅ Pages generated on-demand

Disadvantages:

  • ❌ First users may see stale data
  • ❌ Wymaga konfiguracji czasu rewalidacji
  • ❌ 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):

| Strategia | TTFB | Ocena | |-----------|------|-------| | SSG | 50-100ms | ⭐⭐⭐⭐⭐ Excellent | | ISR (cached) | 50-150ms | ⭐⭐⭐⭐ Bardzo dobry | | ISR (revalidating) | 200-500ms | ⭐⭐⭐ Dobry | | SSR | 300-800ms | ⭐⭐ Average | | SSR (slow API) | 1000ms+ | ⭐ Poor |

1// Measuring TTFB in the browser
2performance.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:

| Strategia | LCP | Ocena | |-----------|-----|-------| | SSG | 0.5-1.5s | ⭐⭐⭐⭐⭐ Excellent | | ISR | 0.8-2s | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 1.5-3s | ⭐⭐⭐ Dobry | | SSR (heavy) | 3s+ | ⭐⭐ Wymaga optymalizacji |

Rekomendacje Google:

  • Dobry: < 2.5s
  • Wymaga poprawy: 2.5s - 4s
  • Poor: > 4s
1// Monitoring LCP
2import { onLCP } from 'web-vitals';
3
4onLCP((metric) => {
5  console.log('LCP:', metric.value);
6  // Send to analytics
7  analytics.track('Web Vitals', {
8    metric: 'LCP',
9    value: metric.value,
10    rating: metric.rating
11  });
12});

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:

| Strategia | CLS | Ocena | |-----------|-----|-------| | SSG | 0-0.05 | ⭐⭐⭐⭐⭐ Excellent | | ISR | 0-0.1 | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 0.1-0.25 | ⭐⭐⭐ Good (if well optimized) |

Rekomendacje Google:

  • Dobry: < 0.1
  • Wymaga poprawy: 0.1 - 0.25
  • Poor: > 0.25
1// Monitoring CLS
2import { onCLS } from 'web-vitals';
3
4onCLS((metric) => {
5  console.log('CLS:', metric.value);
6  analytics.track('Web Vitals', {
7    metric: 'CLS',
8    value: metric.value,
9    rating: metric.rating
10  });
11});

When to Use Which Strategy?

Wybierz SSG gdy:

Blog lub strona marketingowa

  • Content changes rarely
  • SEO jest priorytetem
  • You need the best performance
1// app/blog/[slug]/page.tsx - Idealny dla SSG
2export async function generateStaticParams() {
3  const posts = await getPosts();
4  return posts.map(post => ({ slug: post.slug }));
5}
6
7export default async function BlogPost({ params }) {
8  const post = await getPost((await params).slug);
9  return <Article post={post} />;
10}

Strona dokumentacji

  • Static or rarely updated content
  • Multilingual support
  • Loading speed is critical

Landing pages

  • Marketing campaigns
  • Products/services
  • High conversion = fast loading

Wybierz ISR gdy:

E-commerce (product pages)

  • Data changes regularly (prices, stock)
  • Large number of products
  • Balance between freshness and performance
1// app/products/[id]/page.tsx - Idealny dla ISR
2export const revalidate = 300; // 5 minut
3
4export default async function Product({ params }) {
5  const product = await getProduct((await params).id);
6
7  return (
8    <div>
9      <h1>{product.name}</h1>
10      <p>Cena: {product.price}</p>
11      <p>W magazynie: {product.stock}</p>
12    </div>
13  );
14}

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

Wybierz SSR gdy:

Dashboard / Panel administracyjny

  • Dane w czasie rzeczywistym
  • Personalizacja
  • User authorization
1// app/dashboard/page.tsx - Idealny dla SSR
2export const dynamic = 'force-dynamic';
3
4export default async function Dashboard() {
5  const session = await getServerSession();
6  const userData = await getUserData(session.userId);
7
8  return (
9    <div>
10      <h1>Witaj, {userData.name}</h1>
11      <RealTimeAnalytics userId={session.userId} />
12    </div>
13  );
14}

Search results

  • Dynamiczne zapytania
  • Personalizowane wyniki
  • Always fresh data

User-specific content

  • Koszyk zakupowy
  • User preferences
  • Activity history

Real-world Examples with Benchmarks

Example 1: Tech Blog (SSG)

1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5  const post = await getPost((await params).slug);
6
7  return {
8    title: post.title,
9    description: post.excerpt,
10    openGraph: {
11      images: [post.coverImage],
12    },
13  };
14}
15
16export async function generateStaticParams() {
17  const posts = await getAllPosts();
18
19  return posts.map((post) => ({
20    slug: post.slug,
21  }));
22}
23
24async function getPost(slug: string) {
25  const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
26    cache: 'force-cache'
27  });
28  return res.json();
29}
30
31export default async function BlogPost({ params }) {
32  const post = await getPost((await params).slug);
33
34  return (
35    <article>
36      <h1>{post.title}</h1>
37      <time>{post.publishedAt}</time>
38      <Image
39        src={post.coverImage}
40        alt={post.title}
41        width={1200}
42        height={630}
43        priority
44      />
45      <div dangerouslySetInnerHTML={{ __html: post.content }} />
46    </article>
47  );
48}

Metrics:

  • TTFB: ~80ms
  • LCP: ~1.2s
  • CLS: 0.02
  • Build time: 2min for 500 posts
  • Ocena: ⭐⭐⭐⭐⭐

Example 2: E-commerce Marketplace (ISR)

1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4  const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
5    next: { revalidate: 600 } // Rewalidacja co 10 minut
6  });
7
8  if (!res.ok) {
9    notFound();
10  }
11
12  return res.json();
13}
14
15export async function generateStaticParams() {
16  // Generate only top 100 products during build
17  const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
18    .then(res => res.json());
19
20  return topProducts.map((product) => ({
21    id: product.id.toString(),
22  }));
23}
24
25export default async function ProductPage({ params }) {
26  const product = await getProduct((await params).id);
27
28  return (
29    <div className="product-page">
30      <ProductGallery images={product.images} />
31
32      <div className="product-info">
33        <h1>{product.name}</h1>
34
35        <div className="price-stock">
36          <span className="price">{product.price} QC</span>
37          <StockIndicator stock={product.stock} />
38        </div>
39
40        <div className="description">
41          {product.description}
42        </div>
43
44        <AddToCartButton productId={product.id} />
45
46        <ProductReviews productId={product.id} />
47      </div>
48
49      <RelatedProducts categoryId={product.categoryId} />
50    </div>
51  );
52}
53
54export const revalidate = 600; // 10 minut

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
  • Ocena: ⭐⭐⭐⭐

Example 3: Real-time Dashboard (SSR)

1// app/dashboard/analytics/page.tsx
2
3export const dynamic = 'force-dynamic';
4export const fetchCache = 'force-no-store';
5
6async function getRealTimeAnalytics(userId: string) {
7  const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
8    cache: 'no-store',
9    headers: {
10      Authorization: `Bearer ${process.env.API_TOKEN}`
11    }
12  });
13
14  return res.json();
15}
16
17export default async function AnalyticsDashboard() {
18  const session = await getServerSession();
19
20  if (!session) {
21    redirect('/login');
22  }
23
24  const analytics = await getRealTimeAnalytics(session.user.id);
25
26  return (
27    <div className="analytics-dashboard">
28      <h1>Analityka w czasie rzeczywistym</h1>
29
30      <div className="stats-grid">
31        <StatCard
32          title="Active Users"
33          value={analytics.activeUsers}
34          change={analytics.activeUsersChange}
35        />
36
37        <StatCard
38          title="Przychody dzisiaj"
39          value={`${analytics.todayRevenue} QC`}
40          change={analytics.revenueChange}
41        />
42
43        <StatCard
44          title="Konwersje"
45          value={`${analytics.conversionRate}%`}
46          change={analytics.conversionChange}
47        />
48      </div>
49
50      <RevenueChart data={analytics.revenueChart} />
51      <UserActivityFeed data={analytics.recentActivity} />
52    </div>
53  );
54}

Metrics:

  • TTFB: ~450ms
  • LCP: ~2.4s
  • CLS: 0.08
  • Data freshness: Always current
  • Ocena: ⭐⭐⭐ (dobry dla real-time, ale wymaga optymalizacji)

Example 4: Hybrid Approach - Newsfeed (ISR + Client-side updates)

1// app/news/page.tsx
2
3async function getLatestNews() {
4  const res = await fetch('https://api.quantum-news.com/latest', {
5    next: { revalidate: 60 } // Revalidation every minute
6  });
7
8  return res.json();
9}
10
11export default async function NewsFeed() {
12  const initialNews = await getLatestNews();
13
14  return (
15    <div className="news-feed">
16      <h1>News from Metropolis Quantum</h1>
17
18      {/* Server-rendered initial content (ISR) */}
19      <NewsList initialNews={initialNews} />
20
21      {/* Client-side updates co 30 sekund */}
22      <ClientNewsUpdater />
23    </div>
24  );
25}
26
27export const revalidate = 60;
1// components/ClientNewsUpdater.tsx
2'use client';
3
4import { useEffect, useState } from 'react';
5
6export function ClientNewsUpdater() {
7  const [latestNews, setLatestNews] = useState([]);
8
9  useEffect(() => {
10    const interval = setInterval(async () => {
11      const res = await fetch('/api/news/latest');
12      const data = await res.json();
13      setLatestNews(data);
14    }, 30000); // 30 sekund
15
16    return () => clearInterval(interval);
17  }, []);
18
19  if (latestNews.length === 0) return null;
20
21  return (
22    <div className="breaking-news">
23      <h2>Latest News</h2>
24      {latestNews.map(news => (
25        <NewsCard key={news.id} news={news} />
26      ))}
27    </div>
28  );
29}

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 w Next.js 15

1// app/layout.tsx
2import { SpeedInsights } from '@vercel/speed-insights/next';
3import { Analytics } from '@vercel/analytics/react';
4
5export default function RootLayout({ children }) {
6  return (
7    <html lang="pl">
8      <body>
9        {children}
10        <SpeedInsights />
11        <Analytics />
12      </body>
13    </html>
14  );
15}

2. Custom Web Vitals reporting

1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7  useReportWebVitals((metric) => {
8    // Sending to your own analytics
9    const body = JSON.stringify({
10      name: metric.name,
11      value: metric.value,
12      rating: metric.rating,
13      delta: metric.delta,
14      id: metric.id,
15    });
16
17    fetch('/api/analytics/web-vitals', {
18      method: 'POST',
19      headers: { 'Content-Type': 'application/json' },
20      body,
21    });
22  });
23
24  return null;
25}

3. Lighthouse CI w pipeline

1# .github/workflows/lighthouse.yml
2name: Lighthouse CI
3
4on:
5  pull_request:
6    branches: [main]
7
8jobs:
9  lighthouse:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v3
13      - uses: actions/setup-node@v3
14
15      - name: Install dependencies
16        run: npm ci
17
18      - name: Build
19        run: npm run build
20
21      - name: Run Lighthouse CI
22        run: |
23          npm install -g @lhci/cli
24          lhci autorun
25
26      - name: Upload results
27        uses: actions/upload-artifact@v3
28        with:
29          name: lighthouse-results
30          path: .lighthouseci

Optimization for Each Strategy

SSG Optimizations

1// next.config.js
2module.exports = {
3  images: {
4    formats: ['image/avif', 'image/webp'],
5    deviceSizes: [640, 750, 828, 1080, 1200],
6  },
7  compress: true,
8  poweredByHeader: false,
9  generateEtags: true,
10};

ISR Optimizations

1// Use on-demand revalidation for important updates
2// app/api/revalidate/route.ts
3import { revalidatePath } from 'next/cache';
4import { NextRequest } from 'next/server';
5
6export async function POST(request: NextRequest) {
7  const secret = request.nextUrl.searchParams.get('secret');
8
9  if (secret !== process.env.REVALIDATION_TOKEN) {
10    return Response.json({ message: 'Invalid token' }, { status: 401 });
11  }
12
13  const path = request.nextUrl.searchParams.get('path');
14
15  if (path) {
16    revalidatePath(path);
17    return Response.json({ revalidated: true });
18  }
19
20  return Response.json({ message: 'Missing path' }, { status: 400 });
21}

SSR Optimizations

1// Use streaming for faster TTFB
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4
5export default function Dashboard() {
6  return (
7    <div>
8      <h1>Dashboard</h1>
9
10      <Suspense fallback={<AnalyticsSkeleton />}>
11        <Analytics />
12      </Suspense>
13
14      <Suspense fallback={<ChartSkeleton />}>
15        <RevenueChart />
16      </Suspense>
17    </div>
18  );
19}

Summary i Decision Tree

1Must data be real-time?
2├─ TAK → SSR
3│   └─ Consider streaming and Suspense for better performance
45└─ NIE
67    ├─ Does data change more often than once per hour?
8    │   ├─ TAK → ISR (revalidate: 60-3600)
9    │   │   └─ Consider on-demand revalidation for important updates
10    │   │
11    │   └─ NIE → SSG
12    │       └─ Use generateStaticParams for all possible paths
1314    └─ Czy potrzebujesz personalizacji?
15        ├─ TAK → SSR lub ISR + Client Components
16        └─ NIE → SSG lub ISR

Kluczowe wnioski:

  1. SSG - najszybszy, ale statyczny (blogi, marketing, dokumentacja)
  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!

Go to CodeWorlds