Rendering in Next.js App Router: SSG, ISR, SSR, CSR en PPR Uitgelegd

Leer alle rendering strategieën in Next.js 16 App Router: SSG, ISR, SSR, CSR en Partial Prerendering (PPR). Met werkende codevoorbeelden, de nieuwe use cache-directive en een praktische beslisboom.

Rendering strategieën begrijpen in Next.js App Router

Oké, laten we het even hebben over een van de krachtigste (en eerlijk gezegd soms meest verwarrende) aspecten van Next.js: rendering strategieën. In de App Router heb je vijf fundamentele benaderingen tot je beschikking: Static Site Generation (SSG), Incremental Static Regeneration (ISR), Server-Side Rendering (SSR), Client-Side Rendering (CSR) en het relatief nieuwe Partial Prerendering (PPR).

Elke strategie heeft z'n eigen sterke punten als het gaat om performance, SEO en gebruikerservaring. En het mooie is: je hoeft niet één keuze te maken voor je hele app.

In dit artikel nemen we ze allemaal door. Je leert wanneer je welke strategie inzet, hoe je ze implementeert met de nieuwste Next.js 16 API's, en hoe je ze slim combineert. We werken met concrete codevoorbeelden, behandelen de nieuwe use cache-directive en Cache Components, en ik geef je een praktische beslisboom die je gewoon kunt volgen.

Static Site Generation (SSG): maximale snelheid voor stabiele content

SSG is eigenlijk de simpelste strategie om te begrijpen. Je pagina's worden gegenereerd tijdens het build-proces — de HTML wordt vooraf aangemaakt en geserveerd als statische bestanden, rechtstreeks vanaf een CDN. Het resultaat? De snelst mogelijke laadtijden en uitstekende SEO.

In de App Router is SSG het standaardgedrag. Zolang je component geen dynamische data ophaalt (of data ophaalt met force-cache), wordt de pagina automatisch statisch gegenereerd. Daar hoef je verder niks voor te doen.

SSG implementeren in de App Router

// app/blog/page.tsx
export default async function BlogPage() {
  // Statisch gegenereerd — data wordt opgehaald tijdens build
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache',
  }).then(res => res.json());

  return (
    <main>
      <h1>Blog</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

Dynamische routes statisch genereren met generateStaticParams

Voor dynamische routes zoals /blog/[slug] gebruik je generateStaticParams om tijdens de build alle mogelijke paden te genereren. Ik gebruik dit zelf voor vrijwel elke blog-achtige structuur:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(
    res => res.json()
  );

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetch(
    `https://api.example.com/posts/${slug}`,
    { cache: 'force-cache' }
  ).then(res => res.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Wanneer SSG gebruiken

  • Marketingpagina's en landingspagina's
  • Blogartikelen en documentatie
  • Portfoliopagina's en andere statische content
  • Alles waar de content niet verandert tussen deploys

Incremental Static Regeneration (ISR): statische snelheid met verse data

ISR is wat mij betreft een van de slimste features van Next.js. Het combineert het beste van SSG en SSR: pagina's worden statisch gegenereerd, maar kunnen op de achtergrond opnieuw worden gegenereerd na een ingesteld tijdsinterval.

Gebruikers krijgen altijd een snelle, gecachte versie te zien. Ondertussen wordt de content op de achtergrond bijgewerkt. Best of both worlds, zeg maar.

Tijdgebaseerde revalidatie

De eenvoudigste manier om ISR te implementeren is met de revalidate-optie. Je kunt dit per fetch instellen of als route-segment-configuratie:

// app/producten/page.tsx
// Optie 1: revalidate per fetch
export default async function ProductenPage() {
  const producten = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // Elke 60 minuten opnieuw valideren
  }).then(res => res.json());

  return (
    <main>
      <h1>Producten</h1>
      {producten.map((product: any) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>€{product.price}</p>
        </div>
      ))}
    </main>
  );
}

// Optie 2: revalidate op route-niveau
export const revalidate = 3600;

On-demand revalidatie met revalidatePath en revalidateTag

Soms wil je niet wachten op een tijdsinterval. Misschien is er net een product bijgewerkt in je CMS, of heeft een gebruiker iets gewijzigd. Dan kun je on-demand revalidatie gebruiken:

// app/producten/page.tsx
export default async function ProductenPage() {
  const producten = await fetch('https://api.example.com/products', {
    next: { tags: ['producten'] }, // Tag voor on-demand revalidatie
  }).then(res => res.json());

  return (
    <main>
      {producten.map((product: any) => (
        <div key={product.id}>{product.name}</div>
      ))}
    </main>
  );
}

// app/actions.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: FormData) {
  await fetch(`https://api.example.com/products/${id}`, {
    method: 'PUT',
    body: data,
  });

  // Methode 1: specifiek pad opnieuw valideren
  revalidatePath('/producten');

  // Methode 2: alle fetches met tag 'producten' invalideren
  revalidateTag('producten');
}

Webhook-gebaseerde revalidatie via Route Handler

Een patroon dat ik in de praktijk vaak tegenkom: een Route Handler die door een extern systeem (zoals een headless CMS) wordt aangeroepen om revalidatie te triggeren. Handig als je met Sanity, Contentful of een vergelijkbaar systeem werkt:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.headers.get('x-revalidation-secret');

  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json(
      { message: 'Ongeldig geheim' },
      { status: 401 }
    );
  }

  const { tag } = await request.json();
  revalidateTag(tag);

  return NextResponse.json({ revalidated: true, now: Date.now() });
}

Wanneer ISR gebruiken

  • E-commerce productcatalogi die regelmatig veranderen
  • Nieuwssites en blogs met frequente updates
  • Contentpagina's die via een CMS worden beheerd
  • Eigenlijk alles waar 'bijna realtime' goed genoeg is

Server-Side Rendering (SSR): verse data bij elk verzoek

Bij SSR wordt de HTML bij elk binnenkomend verzoek op de server gegenereerd. Dat betekent dat gebruikers altijd de meest actuele data zien. De keerzijde? Het kost meer servercapaciteit dan SSG of ISR.

In de App Router activeer je SSR door dynamische functies te gebruiken zoals cookies(), headers() of searchParams, of door caching expliciet uit te schakelen. Next.js detecteert dit automatisch — je hoeft het niet handmatig te configureren.

// app/dashboard/page.tsx
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  // cookies() maakt deze pagina automatisch dynamisch (SSR)
  const cookieStore = await cookies();
  const token = cookieStore.get('session-token')?.value;

  const data = await fetch('https://api.example.com/dashboard', {
    headers: { Authorization: `Bearer ${token}` },
    cache: 'no-store', // Expliciet geen caching
  }).then(res => res.json());

  return (
    <main>
      <h1>Welkom, {data.user.name}</h1>
      <p>Laatste activiteit: {data.lastActivity}</p>
    </main>
  );
}

// Alternatief: forceer dynamisch gedrag op route-niveau
export const dynamic = 'force-dynamic';

SSR combineren met Streaming en Suspense

Dit is waar het echt interessant wordt. In plaats van te wachten tot alle data geladen is, kun je delen van de pagina progressief laden met React Suspense. Gebruikers zien direct de structuur van de pagina, terwijl de data-intensieve onderdelen binnenkomen:

// app/dashboard/page.tsx
import { Suspense } from 'react';
import RecenteOrders from './recente-orders';
import Statistieken from './statistieken';

export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Statistieken laden...</p>}>
        <Statistieken />
      </Suspense>
      <Suspense fallback={<p>Orders laden...</p>}>
        <RecenteOrders />
      </Suspense>
    </main>
  );
}

Wanneer SSR gebruiken

  • Gebruikersspecifieke dashboards en profielpagina's
  • Realtime data zoals voorraadinformatie of live prijzen
  • Pagina's die afhankelijk zijn van cookies of headers
  • Zoekresultaten en gefilterde overzichten

Client-Side Rendering (CSR): interactiviteit in de browser

CSR is de rendering strategie die het dichtst bij een traditionele React-app komt. De server stuurt een minimale HTML-shell en de browser doet het zware werk met JavaScript. In de App Router gebruik je CSR voor componenten die interactiviteit nodig hebben en geen SEO-kritische content bevatten.

Denk aan zoekbalken, filters, formulieren — dat soort dingen.

// app/components/zoekfilter.tsx
'use client';

import { useState, useEffect } from 'react';

export default function ZoekFilter() {
  const [query, setQuery] = useState('');
  const [resultaten, setResultaten] = useState<any[]>([]);

  useEffect(() => {
    if (query.length < 2) return;

    const controller = new AbortController();
    fetch(`/api/zoeken?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then(res => res.json())
      .then(data => setResultaten(data.results))
      .catch(() => {});

    return () => controller.abort();
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Zoeken..."
      />
      <ul>
        {resultaten.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

Wanneer CSR gebruiken

  • Interactieve widgets zoals zoekbalken, filters en formulieren
  • Componenten die browser-API's nodig hebben (localStorage, geolocation)
  • Dashboard-onderdelen die niet SEO-kritisch zijn
  • Realtime chat of notificatiesystemen

Partial Prerendering (PPR): het beste van twee werelden

Nu wordt het pas echt spannend. Partial Prerendering is naar mijn mening de meest interessante rendering strategie in Next.js op dit moment. PPR combineert statische en dynamische content binnen dezelfde route. De server stuurt direct een statische shell, terwijl dynamische onderdelen parallel worden gestreamd zodra ze klaar zijn.

Met Next.js 16 is PPR volwassen geworden dankzij Cache Components en de use cache-directive. De eerdere experimentele experimental.ppr-vlag? Die is verleden tijd.

PPR inschakelen in Next.js 16

Het inschakelen is verrassend simpel. Je hebt alleen deze instelling nodig in je next.config.ts:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;

PPR in de praktijk: statische shell met dynamische gaten

Het kernidee is simpel: statische content wordt direct geserveerd, dynamische onderdelen wikkel je in <Suspense>-boundaries. Next.js regelt de rest:

// app/webshop/page.tsx
import { Suspense } from 'react';
import ProductGrid from './product-grid';
import AanbevolenProducten from './aanbevolen';
import WinkelwagenPreview from './winkelwagen-preview';

export default function WebshopPage() {
  return (
    <main>
      {/* Statisch: wordt direct geserveerd */}
      <h1>Onze Producten</h1>
      <ProductGrid />

      {/* Dynamisch: wordt gestreamd */}
      <Suspense fallback={<p>Aanbevelingen laden...</p>}>
        <AanbevolenProducten />
      </Suspense>

      <Suspense fallback={<p>Winkelwagen laden...</p>}>
        <WinkelwagenPreview />
      </Suspense>
    </main>
  );
}

Cache Components en de use cache-directive

De use cache-directive in Next.js 16 geeft je expliciete controle over caching op pagina-, component- en functieniveau. Dit is een grote verandering ten opzichte van eerdere versies, waar caching grotendeels impliciet was. Nu is het volledig opt-in — en eerlijk gezegd vind ik dat een betere aanpak:

// app/producten/populair.tsx
'use cache';

import { cacheLife } from 'next/cache';

export default async function PopulaireProducten() {
  cacheLife('hours'); // Cache voor uren

  const producten = await db.query(
    'SELECT * FROM products ORDER BY sales DESC LIMIT 10'
  );

  return (
    <section>
      <h2>Populaire Producten</h2>
      {producten.map((product: any) => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <p>€{product.price}</p>
        </div>
      ))}
    </section>
  );
}

Je kunt use cache ook op functieniveau toepassen. Dat is vooral handig voor gedeelde datalogica die je in meerdere componenten gebruikt:

// lib/data.ts
import { cacheLife, cacheTag } from 'next/cache';

export async function getProductCategorieen() {
  'use cache';
  cacheLife('days');
  cacheTag('categorieen');

  return await db.query('SELECT * FROM categories ORDER BY name');
}

export async function getPopulaireProducten() {
  'use cache';
  cacheLife('hours');
  cacheTag('populaire-producten');

  return await db.query(
    'SELECT * FROM products ORDER BY sales DESC LIMIT 10'
  );
}

Wanneer PPR gebruiken

  • E-commerce pagina's met een mix van catalogus (statisch) en persoonlijke aanbevelingen (dynamisch)
  • Dashboards met een vaste lay-out en realtime data
  • Nieuwsartikelen met gepersonaliseerde advertenties of gerelateerde content
  • Eigenlijk elke pagina waar het meeste statisch is, maar kleine delen dynamisch moeten zijn

De juiste strategie kiezen: een praktische beslisboom

Goed, je kent nu alle strategieën. Maar hoe kies je de juiste? Het komt neer op drie kernvragen: hoe vaak verandert de data, is de content gebruikersspecifiek, en hoe belangrijk is SEO?

Ik gebruik zelf de volgende beslisboom:

Beslisboom

  1. Is de content voor alle gebruikers hetzelfde?
    • Ja → Ga naar stap 2
    • Nee → Gebruik SSR of PPR (met dynamische Suspense-boundaries)
  2. Verandert de content tussen deploys?
    • Nee → Gebruik SSG
    • Ja → Ga naar stap 3
  3. Is een kleine vertraging (seconden tot minuten) acceptabel?
    • Ja → Gebruik ISR
    • Nee → Gebruik SSR
  4. Bevat de pagina een mix van statische en dynamische onderdelen?
    • Ja → Gebruik PPR met Cache Components
  5. Is SEO niet belangrijk en is interactiviteit prioriteit?
    • Ja → Gebruik CSR voor die componenten

Overzichtstabel

StrategieRenderingSEOSnelheidData-versheidIdeaal voor
SSGBuild-timeUitstekendSnelstBij deployBlogs, documentatie
ISRBuild + achtergrondUitstekendSnelPeriodiekProductcatalogi, nieuws
SSRPer verzoekGoedGemiddeldRealtimeDashboards, profielen
CSRIn de browserBeperktVariabelRealtimeInteractieve widgets
PPRHybrideUitstekendSnelMixE-commerce, SaaS

Strategieën combineren in één applicatie

En hier zit 'm de echte kracht van Next.js: je hoeft niet één strategie te kiezen voor je hele applicatie. Je kunt per route — en zelfs binnen een route — de optimale aanpak kiezen.

Hier is hoe dat er in de praktijk uitziet voor een e-commerce app:

// Projectstructuur met gemengde strategieën:
//
// app/
// ├── page.tsx              → SSG (homepage, verandert zelden)
// ├── blog/
// │   └── [slug]/page.tsx   → ISR (artikelen, revalidate: 3600)
// ├── producten/
// │   └── page.tsx          → PPR (catalogus statisch + aanbevelingen dynamisch)
// ├── dashboard/
// │   └── page.tsx          → SSR (gebruikersspecifiek)
// └── components/
//     └── zoekbalk.tsx      → CSR ('use client', interactief)

Dit patroon zorgt ervoor dat elke pagina de optimale balans heeft tussen snelheid, data-versheid en SEO. Geen onnodige compromissen.

Veelgestelde vragen

Wat is het verschil tussen ISR en SSR in Next.js?

ISR genereert pagina's statisch en valideert ze op de achtergrond na een ingesteld tijdsinterval. Gebruikers krijgen altijd direct een gecachte versie. SSR daarentegen genereert de HTML bij elk individueel verzoek op de server — langzamer, maar de data is gegarandeerd up-to-date. Kies ISR wanneer een kleine vertraging acceptabel is, en SSR wanneer realtime data echt noodzakelijk is.

Hoe schakel ik Partial Prerendering in voor Next.js 16?

Stel cacheComponents: true in je next.config.ts in en gebruik de use cache-directive in combinatie met <Suspense>-boundaries om statische en dynamische onderdelen te scheiden. De eerdere experimental.ppr-vlag is verwijderd in Next.js 16.

Kan ik verschillende rendering strategieën op dezelfde pagina combineren?

Ja, absoluut — en dat is precies waar PPR voor is bedoeld. Met Cache Components kun je statische onderdelen (navigatie, productlijst) combineren met dynamische onderdelen (winkelwagen, persoonlijke aanbevelingen) op dezelfde pagina. Wikkel de dynamische onderdelen in <Suspense>-boundaries en je bent klaar.

Wat is het verschil tussen revalidatePath en revalidateTag?

revalidatePath invalideert de cache voor een specifiek URL-pad (bijvoorbeeld /producten). revalidateTag invalideert alle gecachte data die is getagd met een bepaalde tag, ongeacht op welke pagina die data wordt gebruikt. Tags zijn flexibeler wanneer dezelfde data op meerdere pagina's voorkomt.

Is SSG nog relevant met de komst van PPR?

Zeker weten. SSG blijft de snelste en goedkoopste optie voor pagina's die volledig statisch zijn — denk aan documentatie, marketingpagina's en blogartikelen. PPR is een aanvulling voor pagina's die overwegend statisch zijn maar kleine dynamische stukken bevatten. Gebruik SSG als de hele pagina statisch kan zijn, en PPR als je een mix nodig hebt.

Over de Auteur Editorial Team

Our team of expert writers and editors.