
High-Performance & Caching-Strategien

Echtes Caching in der Praxis (SSR vs. ISR)
Lass uns ehrlich sein: Ein Headless-Setup aufzusetzen, ist heutzutage nicht mehr die größte Hürde. Die eigentliche Herausforderung beginnt, wenn dein Projekt live geht. Stell dir vor, du baust ein großes Portal für Ferienwohnungen auf Rügen. Du hast unzählige hochauflösende Bilder von Strandvillen, komplexe Galerien und ständig wechselnde Belegungspläne. Wenn in der Hauptsaison Hunderte Urlauber gleichzeitig auf die Seite zugreifen, darf dein Node.js-Server nicht in die Knie gehen.
In der klassischen Webentwicklung greifen wir oft reflexartig zum Server-Side Rendering (SSR). Bei jedem einzelnen Seitenaufruf fragt Next.js brav bei der Contao-API nach den neuesten Daten. Das garantiert zwar absolute Aktualität, aber es zwingt dein Backend bei jedem Klick zur Arbeit. Die Time to First Byte (TTFB) steigt, die Nutzer warten, und die PageSpeed-Werte sinken drastisch.
Die Lösung für genau dieses reale Problem ist Incremental Static Regeneration (ISR). Damit generieren wir die Seite einmal statisch, legen sie in den superschnellen Cache und sagen Next.js: "Hey, behalte diese Seite für eine Stunde im Speicher. Wenn sich danach jemand die Seite ansieht, zeige ihm noch die alte, rasend schnelle Version, aber hole im Hintergrund still und heimlich die neuen Daten von Contao."
So sieht ISR im Next.js 15 Code aus
Graue Theorie bringt uns nicht weiter. Schauen wir uns an, wie wir genau dieses Caching-Verhalten in unserem typsicheren Contao-Fetcher implementieren. Im neuen App Router steuern wir das komplett und sehr elegant über die native fetch-API.
1import { ContaoPageData } from '@/types/contao';
2
3const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
4
5// Ein praxisnaher Fetcher für unsere CMS-Seiten
6export async function getContaoPageData(slug: string): Promise<ContaoPageData | null> {
7 try {
8 const res = await fetch(`${API_BASE_URL}/api/v1/page/${slug}`, {
9 method: 'GET',
10 headers: {
11 'Accept': 'application/json',
12 'Authorization': `Bearer ${process.env.CONTAO_API_KEY}`,
13 },
14 // Hier passiert die Magie: Wir nutzen ISR!
15 next: {
16 // Die Seite wird für 3600 Sekunden (1 Stunde) gecacht
17 revalidate: 3600,
18 // Wir vergeben Tags, um den Cache später gezielt (z.B. per Webhook) leeren zu können
19 tags: ['contao-pages', `page-${slug}`],
20 },
21 });
22
23 if (!res.ok) {
24 console.error(`Contao API antwortete mit Status: ${res.status}`);
25 return null;
26 }
27
28 return await res.json();
29 } catch (error) {
30 console.error('Fehler beim Fetching der Contao-Daten:', error);
31 return null;
32 }
33}Was passiert hier genau?
Kein Server-Stress: Durch
revalidate: 3600fängt Next.js 99 % des Traffics ab. Dein Contao-Backend bekommt von den Tausenden Besuchern fast gar nichts mit.Tags sind deine Lebensversicherung: Das Array
tags: ['contao-pages', \page-${slug]ist in der Praxis extrem wichtig. Wenn ein Immobilienmakler dringend einen Preis korrigiert, wollen wir keine Stunde warten. Über dieses Tag können wir den Cache später per API gezielt in Millisekunden leeren (On-Demand Revalidation).
Mit diesem einfachen, aber mächtigen Fetch-Setup haben wir das Fundament für unsere High-Performance-Seite gegossen. Im nächsten Schritt müssen wir uns um den größten Performance-Killer überhaupt kümmern: Die Bilder.

Bildoptimierung in der Praxis: next/image trifft Contao
In der klassischen Contao-Welt übernimmt das PHP-Backend die volle Kontrolle über deine Medien. Die Bildverarbeitung skaliert Grafiken und generiert das fertige <picture>-Markup anhand der Bildgrößen, die du im Backend definiert hast.
Im Next.js App Router existiert für exakt diesen Zweck die native <Image/> Komponente (next/image). Sie konvertiert Bilder on-the-fly in moderne Formate wie WebP oder AVIF, übernimmt das Lazy Loading und schützt dich durch Breiten- und Höhenangaben vor Layout Shifts (CLS).
Der Headless-Flaschenhals: Wenn wir diese beiden mächtigen Systeme unkoordiniert aufeinander loslassen, optimieren wir Bilder doppelt. Das treibt die CPU-Last des Node-Servers in die Höhe. Noch schlimmer: Wir verlieren redaktionelle Entscheidungen aus dem CMS, wie den in Contao manuell gesetzten "Wichtigen Teil" (Focal Point) für den Zuschnitt.
Die saubere Rollenverteilung: Zwei Wege zum Ziel
Für echte High-Performance in Kundenprojekten etablieren wir eine strikte Gewaltenteilung. In der Praxis haben sich hierfür zwei Varianten bewährt – wobei eine davon unsere absolute Empfehlung ist.
Variante A: Der Next.js-fokussierte Weg (Edge-Rendering)
Contao als Datenlieferant: Das Backend liefert uns im JSON-Payload nur die rohen Dateipfade, die originalen Abmessungen und die redaktionellen Zuschnitts-Vorgaben (Focal Points).
Next.js als Renderer: Unser Frontend nimmt diese Daten, berechnet über next/image die Responsive-Source-Sets und liefert das optimale Bildformat direkt am CDN-Edge aus.
Variante B: Der Contao-fokussierte Weg (Unsere Best-Practice)
Wir bevorzugen in unseren Projekten ganz klar diese zweite Variante. Warum? Machen wir uns nichts vor: Die Image-Rendering-Engine von Contao 5.x ist schlichtweg genial und dem Standard-Handling von Next.js funktional oft überlegen.
Contao als Vollblut-Bildgenerator: Das Backend nutzt seine mächtige ImageFactory und liefert die Bilddaten per API bereits komplett fertig vorgerendert aus.
Sofort startklar: Der JSON-Payload enthält direkt alle nötigen Informationen für ein perfektes Picture-Element – inklusive aller Media-Typen, Breakpoints, WebP/AVIF-Formate und den exakten Auflösungen. Alles ist bereits perfekt berechnet.
Next.js als purer Präsentator: Das Frontend nimmt dieses vorbereitete Datenpaket und gibt es als sauberes HTML-Picture aus. Wir sparen uns die rechenintensive, doppelte Bildoptimierung im Node-Server und nutzen die im Contao-Backend vordefinierten Bildgrößen exakt so, wie sie der Entwickler angelegt und der Redakteur zugeschnitten hat.
Diese Methode ist nicht nur ressourcenschonender, sie bewahrt auch die volle Stärke des bewährten Contao-Bildermanagements in der Headless-Welt.
Praxis & Code: Art Direction und Bild-Ausgabe im Frontend
Je nachdem, für welche der beiden Rollenverteilungen (Variante A oder B) du dich entschieden hast, sieht die Umsetzung in React unterschiedlich aus. Schauen wir uns beide Wege an.
Umsetzung für Variante A: Der Next.js-fokussierte Weg
Wenn dein Design verlangt, dass sich Bilder je nach Viewport komplett verändern – z. B. ein Hochformat-Bild der Ferienwohnung auf dem Smartphone und ein weites Querformat der gesamten Anlage auf dem Desktop –, und du Next.js die Optimierung überlässt, reicht das einfache Image-Tag nicht aus. Next.js bietet hierfür die Funktion getImageProps(), um natives HTML5-Markup mit der Next.js-Bildoptimierung zu generieren.
So sieht die Umsetzung in React aus:
1import { getImageProps } from 'next/image';
2
3interface ResponsiveHeroProps {
4 mobileSrc: string;
5 desktopSrc: string;
6 alt: string;
7}
8
9export default function ResponsiveHero({ mobileSrc, desktopSrc, alt }: ResponsiveHeroProps) {
10 // 1. Props für das mobile Hochformat generieren
11 const { props: mobileProps } = getImageProps({
12 src: mobileSrc,
13 alt: alt || 'Ferienwohnung Detailansicht',
14 width: 600,
15 height: 1200,
16 priority: true, // LCP-Optimierung für das Hero-Bild!
17 });
18
19 // 2. Props für das Desktop Querformat generieren
20 const { props: desktopProps } = getImageProps({
21 src: desktopSrc,
22 alt: alt || 'Ferienwohnung Panorama',
23 width: 1920,
24 height: 800,
25 priority: true,
26 });
27
28 // 3. Sauberes <picture> Element rendern, das Layout-Shifts vermeidet
29 return (
30 <picture>
31 <source media="(max-width: 767px)" srcSet={mobileProps.srcSet} />
32 <source media="(min-width: 768px)" srcSet={desktopProps.srcSet} />
33 <img {...desktopProps} className="w-full h-auto object-cover" />
34 </picture>
35 );
36}Umsetzung für Variante B: Unsere Best-Practice mit Contao-Pre-Rendering
Wenn wir unserem bevorzugten Weg folgen, hat Contao die schwere Arbeit bereits erledigt. Der JSON-Payload unserer API liefert uns ein fertiges Picture-Objekt. Wir verzichten hier ganz bewusst auf die next/image Komponente, um Doppel-Optimierungen zu vermeiden, und rendern stattdessen ein extrem leichtgewichtiges, natives HTML-Picture-Element anhand der perfekten Contao-Vorgaben.
So sieht diese performante React-Komponente aus:
1interface ContaoPictureSource {
2 media?: string;
3 srcSet: string;
4 type?: string;
5}
6
7interface ContaoImageProps {
8 src: string;
9 srcSet?: string;
10 sizes?: string;
11 width: number;
12 height: number;
13 alt: string;
14}
15
16interface ContaoPictureData {
17 img: ContaoImageProps;
18 sources?: ContaoPictureSource[];
19}
20
21export default function ContaoPicture({ data }: { data: ContaoPictureData }) {
22 if (!data || !data.img) return null;
23
24 return (
25 <picture>
26 {/* 1. Wir iterieren durch die von Contao berechneten Source-Elemente (WebP/AVIF, Breakpoints etc.) */}
27 {data.sources && data.sources.map((source, index) => (
28 <source
29 key={index}
30 media={source.media}
31 type={source.type}
32 srcSet={source.srcSet}
33 />
34 ))}
35
36 {/* 2. Das Fallback-Bild, ebenfalls mit den exakten Contao-Abmessungen und Fokus-Punkten */}
37 <img
38 src={data.img.src}
39 srcSet={data.img.srcSet}
40 sizes={data.img.sizes}
41 width={data.img.width}
42 height={data.img.height}
43 alt={data.img.alt || ''}
44 className="w-full h-auto object-cover"
45 loading="lazy"
46 />
47 </picture>
48 );
49}Das Ergebnis für beide Wege: Dein Frontend verliert keine wertvollen Millisekunden beim LCP (Largest Contentful Paint). Der Browser weiß sofort, welches Bild er für die jeweilige Bildschirmbreite herunterladen muss. Mit Variante B sparst du dir zudem wertvolle Server-Ressourcen in Next.js, während Contao weiterhin die volle Kontrolle über das Ausgangsmaterial und die perfekten Zuschnitte behält.

Ereignisgesteuerte Cache-Invalidierung (On-Demand Revalidation)
Wir haben in Abschnitt 1 gesehen, dass die zeitgesteuerte Incremental Static Regeneration (ISR) unseren Server extrem entlastet. Aber sie hat einen Haken in der Praxis: Wenn du revalidate: 3600 setzt, dauern Änderungen im schlimmsten Fall eine Stunde, bis sie online für den Nutzer sichtbar sind. Für Redakteure, die in Contao dringend einen Tippfehler auf der Startseite korrigieren oder einen neuen Preis eintragen, ist diese künstliche Wartezeit inakzeptabel.
Die Enterprise-Lösung dafür heißt On-Demand Revalidation über Webhooks und Cache-Tags.
Das Prinzip ist simpel und extrem effektiv: Wir behalten unseren stundenlangen Cache bei, um den Traffic abzufedern. Aber sobald ein Redakteur in Contao auf "Speichern" klickt, feuert Contao im Hintergrund einen HTTP-POST-Request (einen Webhook) an unser Next.js-Frontend ab. Next.js nimmt diesen Request entgegen und löscht blitzschnell exakt das Tag aus dem Cache, das zu dieser einen Seite gehört.
Praxis & Code: Die Revalidation-Route im App Router
Damit Next.js diesen Webhook von Contao verarbeiten kann, bauen wir einen geschützten API-Endpunkt. Lege dazu im Next.js App Router eine neue Route an.
Datei: src/app/api/revalidate/route.ts
1import { revalidateTag } from 'next/cache';
2import { NextRequest } from 'next/server';
3
4export async function POST(request: NextRequest) {
5 // 1. Das geheime Token aus den Headern auslesen, damit niemand Fremdes unseren Cache leert
6 const secret = request.headers.get('x-revalidate-token');
7
8 // 2. Sicherheits-Check gegen unsere Umgebungsvariable
9 if (secret !== process.env.REVALIDATION_SECRET) {
10 return new Response('Unauthorized', { status: 401 });
11 }
12
13 try {
14 // 3. Den Payload auslesen, um zu erfahren, welches Tag geleert werden soll
15 const payload = await request.json();
16 const tag = payload.tag;
17
18 if (!tag) {
19 return new Response('Missing tag in payload', { status: 400 });
20 }
21
22 // 4. Die native Next.js Funktion aufrufen, um den Cache für dieses Tag zu verwerfen
23 revalidateTag(tag);
24
25 return new Response(`Cache für Tag "${tag}" erfolgreich geleert.`, { status: 200 });
26 } catch (error) {
27 return new Response('Invalid JSON payload', { status: 400 });
28 }
29}Wie greift das nun ineinander? Erinnerst du dich an unseren Fetcher aus Abschnitt 1? Dort haben wir die Tags definiert: tags: ['contao-pages', \page-${slug].
Wenn der Redakteur nun die Seite "Team" ändert, sendet Contao automatisch einen Webhook an [https://dein-frontend.de/api/revalidate](https://dein-frontend.de/api/revalidate) mit dem Sicherheits-Header x-revalidate-token und dem JSON-Body { "tag": "page-team" }.
Next.js löscht daraufhin nur den HTML-Cache für genau diese Team-Seite. Die restlichen Seiten bleiben sicher im Cache. Beim allernächsten Seitenaufruf generiert Next.js die Team-Seite im Hintergrund neu. Das Ergebnis: Maximale Performance durch ISR, aber Updates in Echtzeit wie beim klassischen SSR.

Fazit – Architektur für die Praxis
Eine Headless-Website ist nur dann "Enterprise-Ready", wenn sie unter Last nicht zusammenbricht und Redakteure trotzdem flüssig und ohne künstliche Wartezeiten arbeiten können. In diesem 11. Teil der Masterclass haben wir genau diesen Spagat in der Praxis gelöst:
Smarte Bildoptimierung: Wir haben gelernt, dass wir nicht blind auf die Standard-Tools von Next.js vertrauen müssen. Anstatt den Node-Server mit doppelter Bildberechnung zu belasten (Variante A), setzen wir auf die bewährte Stärke von Contao 5.x (Variante B). Contao liefert uns das perfekt vorbereitete Picture-Element inklusive aller Breakpoints, WebP/AVIF-Formate und Fokus-Punkte direkt über die API. Das Next.js-Frontend kümmert sich nur noch um das pfeilschnelle und leichtgewichtige Rendering.
Caching mit ISR: Wir haben klassischem Server-Side Rendering (SSR) den Rücken gekehrt. Stattdessen nutzen wir Incremental Static Regeneration, um unser Contao-Backend vor Traffic-Spitzen, wild gewordenen Crawlern und Origin Stampedes zu schützen.
Echtzeit-Updates: Durch die Kombination von revalidateTag in Next.js und Webhooks aus Contao vereinen wir das Beste aus beiden Welten: Die kompromisslose Auslieferungsgeschwindigkeit statischer Seiten und die direkte Flexibilität dynamischer CMS-Updates in Echtzeit.
Dein Next.js Frontend ist nun rasend schnell, ressourcenschonend und optimal an den Redaktions-Workflow des CMS angebunden.
Contao Headless Masterclass: High-Performance mit Next.js Pillar
Contao Headless Architektur: Das Konzept im Detail verstehen
Projektstruktur & Entwicklungsumgebung für Contao und Next.js
Contao Headless Basis-Setup: Installation & Bildkonfiguration
Contao Headless Datenstruktur & Redakteurs-Erlebnis
Die API From Scratch entwickeln: Symfony Routing in Contao 5
Contao Headless API-Sicherheit & Formularverarbeitung
Next.js Setup & Grundstruktur für dein Headless CMS
Dynamisches Routing & Navigation in Next.js
Module & Formulare im Frontend rendern
Der Draft- & Preview-Modus im Headless CMS
High-Performance & Caching-Strategien
Häufig gestellte Fragen (FAQ)
Wenn der POST-Request von Contao zu Next.js (z. B. wegen eines Netzwerkfehlers oder eines Timeouts) nicht durchgeht, wird der Cache im Frontend nicht geleert. Das ist der Grund, warum wir als Fallback trotzdem ein zeitbasiertes revalidate (z. B. 3600 Sekunden) in unserem Fetcher setzen sollten. So stellt das System sicher, dass die Seite spätestens nach einer Stunde (oder deinem definierten Intervall) aktualisiert wird, selbst wenn der Webhook einmal ausfallen sollte.
Ja, aber Next.js blockiert externe Bildquellen standardmäßig aus Sicherheitsgründen. Du musst die Domain (oder Subdomain) deines Contao-Backends zwingend in deiner Next.js-Konfiguration (in der next.config.ts oder next.config.js) unter images.remotePatterns auf die Whitelist setzen, damit die Bildoptimierung funktioniert und die Bilder nicht blockiert werden.
Weil SSR bei jedem Klick den Server zwingt, auf die Antwort des Contao-Backends zu warten. Bei komplexen Seiten oder hohem Traffic (z. B. durch Google-Crawler oder Besucher-Spitzen) steigen deine Serverkosten enorm an, die Time to First Byte (TTFB) verschlechtert sich spürbar und im schlimmsten Fall reißt du dein eigenes CMS durch Überlastung (Origin Stampede) offline. ISR bietet dir die perfekte Mischung aus der Geschwindigkeit statischer Seiten und der Flexibilität dynamischer Daten.
Dein nächster Schritt: Phase 4 – SEO-Masterclass & Schema-Validierung
Unsere Headless-Architektur ist jetzt rasend schnell, stabil und für Redakteure nahtlos bedienbar. Doch die beste und schnellste Website der Welt nützt nichts, wenn sie von Google nicht korrekt verstanden und nicht auf den vordersten Plätzen gerankt wird.
In der Headless-Welt gibt es kein klassisches Contao-Plugin, das dir automatisch perfekte Meta-Tags und strukturierte Daten in den unsichtbaren Head-Bereich der Seite zaubert. Wir müssen dem Next.js App Router beibringen, unsere CMS-Daten in sauberes SEO-Gold zu verwandeln.
Mit dem Start von Phase 4 widmen wir uns voll und ganz der Sichtbarkeit. Im kommenden Teil 12: SEO-Masterclass & Schema-Validierung lernst du, wie du eine kugelsichere SEO-Architektur aufbaust:
Dynamische Meta-Tags: Wie du Title, Meta-Descriptions und Open Graph (OG) Images über die
generateMetadata-API im Next.js App Router dynamisch aus dem Contao-Payload generierst.Die XML-Sitemap: Wie du mit der nativen
sitemap.tsin Next.js eine automatisierte, immer aktuelle Sitemap deiner Headless-Inhalte für die Google Search Console bereitstellst.Strukturierte Daten (JSON-LD): Wie du Local Business, Artikel oder FAQ-Schemas dynamisch in React injizierst, um begehrte Rich Snippets in den Suchergebnissen zu triggern.
Qualitätssicherung: Wie wir unsere dynamisch generierten JSON-LD Daten mit dem dedizierten Schema-Tester unter webinteger.dev/tools/schema-tester auf Herz und Nieren prüfen, bevor wir sie live schalten.
Jetzt starten: Teil 12 – SEO-Masterclass & Schema-Validierung im Headless CMS

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


