Next.js API Performance-Optimierung: Caching, Streaming und Edge-Computing in der Praxis

Freitagabend um neun schickte der Product Manager einen Screenshot in die Gruppe. Im Usertest-Video tippte ein Tester auf dem Handy die Blog-Liste an – das Loading-Icon drehte sich volle fünf Sekunden, der Bildschirm blieb weiß. Unten rechts stand: „Aus welchem Jahrzehnt stammt diese Website?”
Ich öffnete Chrome DevTools – der API-Request brauchte 3200 ms. Ich wusste, dass der Endpoint langsam war, hatte die Optimierung aber immer aufgeschoben. So schlimm hatte ich es nicht erwartet.
Zwei Tage später, mit den richtigen Caching-, Streaming- und Edge-Patterns, lagen die Antwortzeiten unter 500 ms. Wichtiger noch: Ich hatte endlich eine Karte dafür, welches Werkzeug zu welchem Problem passt – nicht nur eine Buzzword-Liste.
Heute geht es um drei Hebel: Caching-Strategie wählen, Streaming-Responses umsetzen und wann Edge Functions sich lohnen. Der Code lief in Produktion; die Zahlen sind gemessen.
Warum ist Ihre Next.js API so langsam?
Zuerst die typischen Performance-Engpässe. Beim Debuggen des 3-Sekunden-Endpoints fand ich mehrere klassische Probleme:
Nicht optimierte Datenbankabfragen. Im Code lief eine Schleife, die für jeden Artikel die Autoreninfo separat abfragte – das klassische N+1-Problem. Bei 100 Artikeln sind das 100 DB-Requests. Wie soll das schnell sein? Schlimmer: Manche Tabellen hatten nicht einmal Indizes.
Überhaupt kein Caching. Bei jedem Seiten-Refresh fragte der Server die DB neu ab, rechnete neu, formatierte neu. Konfigurationsdaten, die sich monatlich ändern, wurden jede Sekunde neu berechnet.
Alle Daten auf einmal zurückgeben. Der Endpoint lieferte den vollständigen Inhalt von 100 Artikeln – inklusive Fließtext. Die JSON-Response war über 2 MB groß, allein die Übertragung dauerte eine Sekunde. Für die Listenansicht braucht man nur Titel und Zusammenfassung.
Geografischer Standort des Servers. Wir hatten auf der US-Westküste deployed. Für Nutzer in China allein der Roundtrip mindestens 200 ms – plus GFW-Einfluss … darüber reden wir lieber nicht.
Caching-Änderungen in Next.js 16
Im Oktober 2025 brachte Next.js 16 ein wichtiges Update: von implizitem zu explizitem Caching.
Früher cachte Next.js vieles automatisch – klingt praktisch, führte aber oft zu Verwirrung: Ist das gecacht? Wie lange? Wie leere ich den Cache? Daten waren aktualisiert, die Seite zeigte noch alte Werte – stundenlanges Debuggen, bis der Cache schuld war.
Jetzt müssen Sie Next.js explizit sagen, was gecacht wird und wie lange. Etwas mehr Aufwand, dafür wissen Sie, was passiert – deutlich kontrollierbarer.
Drei Richtungen für Performance-Optimierung
Sobald die Probleme klar waren, auch die Lösungsrichtungen:
- Caching: Bereits Erledigtes nicht wiederholen
- Streaming-Response: Während der Berechnung senden, nicht erst warten bis alles fertig ist
- Edge-Computing: Den Server näher an die Nutzer bringen
Im Folgenden zu jedem Punkt im Detail.
Caching-Strategien: Die richtige Methode spart Arbeit
Next.js kennt vier Cache-Typen: Request Memoization, Data Cache, Full Route Cache, Router Cache. Beim ersten Lesen der Doku war ich überfordert.
Für API Routes reicht meist Data Cache – DB-Ergebnisse oder externe API-Responses zwischenspeichern.
Szenario 1: Statische Daten cachen
Website-Config, Kategorielisten – Daten, die sich selten ändern, können stundenlang gecacht werden.
// app/api/categories/route.js
export async function GET() {
const data = await fetch('https://api.example.com/categories', {
next: { revalidate: 3600 } // 1 Stunde cachen
})
return Response.json(await data.json())
}
So einfach. revalidate: 3600 bedeutet: 1 Stunde cachen, danach automatisch aktualisieren.
Szenario 2: Nutzerbezogene Daten cachen
Profildaten ändern sich selten, dürfen aber nicht ewig veraltet sein. Hier hilft stale-while-revalidate:
// app/api/user/profile/route.js
export async function GET(request) {
const user = await getUserFromDB()
return new Response(JSON.stringify(user), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=60, stale-while-revalidate=300'
}
})
}
Clevere Strategie: Zuerst gecachte (ggf. leicht veraltete) Daten zurückgeben, parallel im Hintergrund aktualisieren. Nutzer spüren keine Verzögerung, Daten bleiben akzeptabel frisch.
s-maxage=60: 60 Sekunden „frisch“. stale-while-revalidate=300: Weitere 300 Sekunden alte Daten nutzbar, während im Hintergrund aktualisiert wird.
Szenario 3: Echtzeitdaten nicht cachen
Aktienkurse, Chat-Nachrichten – hohe Echtzeitanforderung. Entweder gar nicht cachen oder WebSocket / Server-Sent Events.
export async function GET() {
const price = await getStockPrice()
return new Response(JSON.stringify(price), {
headers: {
'Cache-Control': 'no-store' // nicht cachen
}
})
}
Cache-Invalidierung: Was tun nach Datenaktualisierung?
Nutzer aktualisiert sein Profil, der Cache zeigt noch alte Werte? Manuell leeren.
Next.js bietet revalidateTag und revalidatePath:
// app/api/user/update/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const data = await request.json()
await updateUserProfile(data)
// Nutzerbezogenen Cache leeren
revalidateTag('user-profile')
return Response.json({ success: true })
}
Im Query-Endpoint Tags setzen:
export async function GET() {
const data = await fetch('db-api/user', {
next: {
revalidate: 3600,
tags: ['user-profile'] // Tag
}
})
return Response.json(await data.json())
}
Nach Profil-Update ist der Cache sofort ungültig – der nächste Request liefert frische Daten.
Häufige Fallstricke
Fallstrick 1: Zu viel cachen. Bestellstatus 1 Stunde gecacht – Nutzer sieht nach Zahlung lange kein Update. Cache-Dauer muss zur Datenart passen, länger ist nicht immer besser.
Fallstrick 2: Cache-Warming vergessen. Der erste Request bleibt langsam, weil der Cache leer ist. Nach Deployment einmal aktiv aufrufen, Hotspot-Daten vorladen.
Fallstrick 3: Schlechtes Cache-Key-Design. Nutzer A gecacht, Nutzer B bekommt As Daten. Cache-Key muss Nutzer-ID o. Ä. enthalten.
Streaming-Response: Große Datenübertragungen ohne Ruckler
Caching löst Wiederholberechnungen – manche Daten sind aber von Natur aus langsam oder groß. Dann hilft Streaming.
Was ist eine Streaming-Response?
Klassische API-Response wie im Restaurant: Der Koch serviert erst, wenn alle 10 Gerichte fertig sind. Sie warten auf das langsamste.
Streaming: Jedes fertige Gericht kommt sofort – Sie essen schon, während der Rest gekocht wird. Gesamtzeit ähnlich, aber Sie warten nicht hungrig auf einen leeren Tisch.
Für Nutzer: Statt 3 Sekunden weißer Bildschirm sehen sie nach 500 ms die ersten Einträge und können schon browsen.
Wann Streaming nutzen?
Typische Szenarien:
- Lange Listen: Produktlisten, Artikellisten, Suchergebnisse
- KI-generierte Inhalte: ChatGPT-Typing-Effekt ist Streaming
- Große Dateiverarbeitung: Excel-Export, PDF-Generierung
- Echtzeit-Logs: Build-Logs, Task-Fortschritt
Faustregel: Bei großen Datenmengen oder langer Berechnung lohnt Streaming.
Umsetzung in Next.js
Am häufigsten mit ReadableStream:
// app/api/posts/stream/route.js
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// Daten in Batches holen
for (let page = 0; page < 5; page++) {
// Jeweils 20 Einträge
const posts = await fetchPostsFromDB({ page, limit: 20 })
// Batch senden
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
// Verarbeitungszeit simulieren
await new Promise(r => setTimeout(r, 100))
}
// Fertig
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked'
}
})
}
Kernschritte:
ReadableStreamerstellen- In
startDaten in Batches holen - Mit
controller.enqueue()senden - Am Ende
controller.close()
Empfang im Frontend
async function fetchStreamData() {
const response = await fetch('/api/posts/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let allPosts = []
while (true) {
const { done, value } = await reader.read()
if (done) {
console.log('Daten vollständig empfangen')
break
}
// Dekodieren
const chunk = decoder.decode(value)
// JSON parsen (eine Zeile pro Batch)
const posts = JSON.parse(chunk)
allPosts = [...allPosts, ...posts]
// UI live aktualisieren
updatePostList(allPosts)
}
}
Die Liste füllt sich schrittweise – statt langer weißer Bildschirm.
Praxisvergleich
Nach Streaming für die Blog-Liste:
Gesamtzeit nur 1300 ms schneller – wahrgenommen deutlich mehr. Nach 500 ms ist Interaktion möglich, der Rest läuft im Hintergrund.
Kleiner Tipp
Bei sehr großen Datenmengen Virtual Scrolling kombinieren – nur sichtbare Zeilen rendern. React: react-window oder react-virtualized, Vue: vue-virtual-scroller.
Edge Functions: API direkt vor die Haustür der Nutzer
Caching und Streaming optimieren Software – noch direkter: Server näher an die Nutzer.
Einfluss der physischen Distanz
Latenz kommt vor allem von der Entfernung. Lichtgeschwindigkeit ist begrenzt – Peking bis US-Westküste mindestens 200 ms Roundtrip, physikalisch nicht wegzuoptimieren.
Früher ein fester Standort, z. B. Alibaba Cloud Peking – schnell für Peking, langsam für New York.
Edge Functions: Code auf Dutzende bis Hunderte Knoten weltweit – Routing zum nächsten. Peking-Nutzer → Peking-Knoten, New-York-Nutzer → New-York-Knoten, Latenz unter 50 ms.
Edge Runtime vs. Node.js Runtime
API Routes laufen standardmäßig auf Node.js Runtime – voller Zugriff auf fs, crypto, DB-Verbindungen usw.
Edge Runtime basiert auf V8 (wie Chrome), nicht vollständiges Node.js. Vorteil: Start in 0–5 ms. Nachteil: Viele Node.js-APIs fehlen.
| Eigenschaft | Node.js Runtime | Edge Runtime |
|---|---|---|
| Startzeit | 100–500 ms | 0–5 ms |
| Verfügbare APIs | Alle Node.js-APIs | Eingeschränkt (Web-Standard) |
| Einsatz | Komplexe Business-Logik, DB | Leichte Logik, Auth, Proxy |
| Globale Latenz | Abhängig vom Standort | Weltweit <50 ms |
| Speicherlimit | Höher | Niedriger (128 MB) |
Wann Edge Functions sinnvoll sind?
Nicht jede API gehört an den Edge. Typische Szenarien:
Szenario 1: Authentifizierung
JWT prüfen, API-Key validieren – leichte Logik am Edge, ungültige Requests erreichen den zentralen Server nicht.
// app/api/auth/route.js
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
// Token prüfen (jose-Bibliothek, Edge-kompatibel)
const isValid = await verifyToken(token)
if (!isValid) {
return new Response('Invalid token', { status: 401 })
}
return Response.json({ user: 'authenticated' })
}
Szenario 2: Geolocation-Personalisierung
Inhalte nach IP: Sprache, Währung, Empfehlungen.
export const runtime = 'edge'
export async function GET(request) {
// Geolocation (Vercel injiziert automatisch)
const country = request.geo?.country || 'US'
const city = request.geo?.city || 'Unknown'
const content = getLocalizedContent(country)
return Response.json({
country,
city,
content,
currency: country === 'CN' ? 'CNY' : 'USD'
})
}
Keine DB nötig – am Edge, sehr schnell.
Szenario 3: API-Proxy
Mehrere externe APIs am Edge aggregieren, weniger Client-Requests.
export const runtime = 'edge'
export async function GET(request) {
const [weather, news] = await Promise.all([
fetch('https://api.weather.com/...'),
fetch('https://api.news.com/...')
])
return Response.json({
weather: await weather.json(),
news: await news.json()
})
}
Ein Request vom Client, parallele Verarbeitung im Backend.
Szenario 4: A/B-Tests
Am Edge entscheiden, welche Version – ohne Änderung an der Hauptanwendung.
export const runtime = 'edge'
export async function GET(request) {
const userId = request.headers.get('x-user-id')
const variant = parseInt(userId) % 2 === 0 ? 'A' : 'B'
const content = variant === 'A' ? getContentA() : getContentB()
return Response.json({ variant, content })
}
Einschränkungen von Edge Functions
Warum nicht alles migrieren?
Limit 1: Keine Node.js-exklusiven APIs
fs, path, child_process funktionieren nicht.
Limit 2: Datenbankverbindungen
Klassische Treiber (pg, mysql2) nutzen Node.js net – am Edge nicht verfügbar. HTTP-basiert:
- Prisma Data Proxy
- PlanetScale (MySQL)
- Supabase (PostgreSQL)
- Redis mit HTTP-API
Limit 3: Speicher und Laufzeit
Typisch 128 MB RAM, 30 Sekunden Laufzeit – ungeeignet für schwere Berechnungen.
Empfehlung: Hybrid
- Edge: Auth, Geolocation, einfacher Proxy
- Zentral (Node.js): Komplexe Logik, DB, Dateiverarbeitung
Edge filtert Ungültiges und Einfaches, Komplexes geht an den zentralen Server – niedrige Latenz ohne Edge-Limits.
Gemessene Performance
Laut Medium-Benchmark:
- Vercel Edge Functions: Ø 48,3 ms
- Cloudflare Workers (Custom): Ø 36,37 ms
- Klassische Node.js API (eine Region): Ø 200–500 ms
Edge ist schnell – konkreter Effekt hängt von der Nutzerverteilung ab. Nur Nutzer in China? Ein Server in China kann schneller sein.
Praxis: Blog-Artikellisten-API optimieren
Die drei Techniken kombiniert – am Beispiel der langsamen Blog-Liste von Anfang an.
Probleme vor der Optimierung
Originalcode:
// app/api/posts/route.js
export async function GET() {
// Problem 1: Jeder Request trifft die DB, kein Cache
const posts = await db.post.findMany({
take: 100,
include: {
author: true, // Problem 2: N+1
tags: true
}
})
// Problem 3: Vollständiger Artikelinhalt, große Payload
return Response.json(posts)
}
Performance:
- Antwortzeit: 2800 ms
- JSON-Größe: 2,3 MB
- UX: 3 Sekunden weißer Bildschirm
Schritt 1: DB-Abfragen optimieren
N+1 beheben, nur nötige Felder:
export async function GET() {
const posts = await db.post.findMany({
take: 100,
select: {
id: true,
title: true,
summary: true, // nur Zusammenfassung, kein Volltext
createdAt: true,
author: {
select: { name: true, avatar: true }
}
}
})
return Response.json(posts)
}
Ergebnis: 800 ms, JSON von 2,3 MB auf 180 KB.
Schritt 2: Caching
Liste ändert sich selten – 5 Minuten cachen:
export async function GET() {
const posts = await db.post.findMany({
// ... wie oben
}, {
next: {
revalidate: 300, // 5 Minuten
tags: ['posts']
}
})
return Response.json(posts)
}
Beim Veröffentlichen Cache leeren:
// app/api/posts/publish/route.js
import { revalidateTag } from 'next/cache'
export async function POST(request) {
const newPost = await request.json()
await db.post.create({ data: newPost })
revalidateTag('posts')
return Response.json({ success: true })
}
Ergebnis: Cache-Hit ~50 ms, Serverlast -90 %.
Schritt 3: Streaming
Erstbesuch ohne Cache noch 800 ms – Streaming:
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const batchSize = 20
for (let page = 0; page < 5; page++) {
const posts = await db.post.findMany({
skip: page * batchSize,
take: batchSize,
select: { /* wie oben */ }
})
const chunk = JSON.stringify(posts) + '\n'
controller.enqueue(encoder.encode(chunk))
}
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'application/x-ndjson',
'Cache-Control': 's-maxage=300, stale-while-revalidate=600'
}
})
}
Ergebnis: Erster Batch nach 300 ms – sofort browsbar, Gesamt ~800 ms ohne Wartegefühl.
Schritt 4: Auth am Edge (optional)
// app/api/posts/route.js (Edge-Auth-Schicht)
export const runtime = 'edge'
export async function GET(request) {
const token = request.headers.get('authorization')
if (!token) {
return new Response('Unauthorized', { status: 401 })
}
return fetch(`${process.env.API_BASE_URL}/posts/internal`, {
headers: { authorization: token }
})
}
Ungültige Requests werden am Edge abgefangen.
Vergleich nach Optimierung
| Kennzahl | Vorher | Nachher | Verbesserung |
|---|---|---|---|
| Erstbesuch Antwortzeit | 2800 ms | 300 ms (erster Batch) | 89 % ↓ |
| Cache-Hit Antwortzeit | – | 50 ms | 98 % ↓ |
| JSON-Größe | 2,3 MB | 180 KB | 92 % ↓ |
| Time-to-Interactive | 2800 ms | 300 ms | 89 % ↓ |
| Serverlast | 100 % | 10 % | 90 % ↓ |
Kein „Aus welchem Jahrzehnt?“ mehr.
Performance-Monitoring und kontinuierliche Optimierung
Optimierung ist kein Endpunkt – ohne Monitoring keine Validierung.
Wichtige Kennzahlen
-
Antwortzeit-Verteilung (P50, P95, P99)
- P50: Median-Erfahrung
- P95: 95 % der Nutzer
- P99: langsamste 1 % (oft Anomalien)
-
Cache-Hit-Rate
- <70 %: Strategie prüfen
- >95 %: evtl. zu lange Cache-Zeit, Daten veralten
-
Fehlerrate
- Nach Optimierung nicht steigen lassen
- Streaming kann mittendrin abbrechen – besonders beachten
-
Geografische Verteilung
- Latenz nach Region
- Entscheidung für Edge Functions
Monitoring-Tools
Vercel Analytics: Bei Vercel-Deployment integriert – Antwortzeit pro API.
Next.js Instrumentation API (2026): Monitoring-Punkte im Code:
// instrumentation.js
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
require('./monitoring')
}
}
// monitoring.js
export function onRequestEnd(info) {
console.log(`API ${info.url} took ${info.duration}ms`)
sendToMonitoring({
url: info.url,
duration: info.duration,
status: info.status
})
}
Eigenes Logging: Einfach, aber wirksam:
export async function GET() {
const start = Date.now()
const data = await fetchData()
const duration = Date.now() - start
console.log(`API /posts took ${duration}ms`)
return Response.json(data)
}
Tipps für laufende Optimierung
- Cache-Strategie regelmäßig prüfen – Business ändert sich, Cache muss mit
- A/B-Tests – unsicher? Testen
- Daten statt Bauchgefühl – Monitoring entscheiden lassen
Performance-Optimierung ist ein Prozess, kein Einmalprojekt.
Fazit
Caching: Nach Datenart wählen. Statisch lang, Nutzerdaten mit stale-while-revalidate, Echtzeit nicht cachen. Nach Updates invalidieren.
Streaming: Bei großen oder langsamen Daten. Früher Inhalt sichtbar statt weißem Bildschirm. Mit Virtual Scrolling noch besser.
Edge Functions: Auth, Geolocation, Proxy – leichte Logik. Komplexes bleibt in Node.js Runtime, Hybrid ist der richtige Weg.
Schrittweise vorgehen: langsamsten Endpoint nehmen, drei Hebel anwenden, messen, nachjustieren.
Meine Blog-Liste: von 3 Sekunden auf 300 ms – spürbar besser. Probieren Sie es: einen langsamen Endpoint wählen und heute anfangen. Fragen gerne in den Kommentaren – gemeinsam weiter.
FAQ
Wann wird der Next.js API-Cache ungültig?
Eignen sich Streaming-Responses für alle Endpoints?
Welche Einschränkungen haben Edge Functions?
Wie wählt man die richtige Caching-Strategie?
Wie validiert man Optimierungsergebnisse?
11 Min. Lesezeit · Veröffentlicht am: 5. Jan. 2026 · Aktualisiert am: 14. Juli 2026
Next.js Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Next.js API Routes – Komplettguide: Von Route Handlers bis zu Best Practices für Fehlerbehandlung
Komplettguide zu Next.js API Routes: Route Handlers erstellen, Requests verarbeiten, Fehlerbehandlung und Response-Design – damit Sie Backend-Schnittstellen mit Next.js sicher entwickeln.
Teil 18 von 51
Nächster
Next.js API-Authentifizierung und Sicherheit: Praxisleitfaden von JWT bis Rate Limiting
Vollständiger Praxisleitfaden zur Next.js API-Sicherheit: JWT-Authentifizierung, CORS-Konfiguration, Rate Limiting und Eingabevalidierung – Schritt für Schritt zu einer produktionsreifen, sicheren Anwendung inklusive aktueller Methoden zur Abwehr von Sicherheitslücken.
Teil 20 von 51
Ähnliche Beiträge
Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick

Next.js App Router Einstieg: Kernkonzepte und Grundlagen im Überblick
Next.js 15 in der Praxis: Wie ich an einem Wochenende ein produktionsreifes Blog-System aufgebaut habe


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen