Design wechseln

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

Easton editorial illustration: state-management shelf

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:

  1. Caching: Bereits Erledigtes nicht wiederholen
  2. Streaming-Response: Während der Berechnung senden, nicht erst warten bis alles fertig ist
  3. 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.

500ms → 50ms
Antwortzeit -90 %
Nach Caching für die Kategorieliste liefern die meisten Requests direkt aus dem Cache – ohne DB-Zugriff

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:

  1. Lange Listen: Produktlisten, Artikellisten, Suchergebnisse
  2. KI-generierte Inhalte: ChatGPT-Typing-Effekt ist Streaming
  3. Große Dateiverarbeitung: Excel-Export, PDF-Generierung
  4. 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:

  1. ReadableStream erstellen
  2. In start Daten in Batches holen
  3. Mit controller.enqueue() senden
  4. 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:

2800ms → 500ms
Time-to-First-Content
Vorher 2800 ms bis alles da; nachher 500 ms für die ersten 20 Artikel – sofort browsbar

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.

EigenschaftNode.js RuntimeEdge Runtime
Startzeit100–500 ms0–5 ms
Verfügbare APIsAlle Node.js-APIsEingeschränkt (Web-Standard)
EinsatzKomplexe Business-Logik, DBLeichte Logik, Auth, Proxy
Globale LatenzAbhängig vom StandortWeltweit <50 ms
SpeicherlimitHöherNiedriger (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' })
}
200ms → 20ms
Auth-Latenz -90 %
Token am Edge prüfen – ungültige Requests belasten den zentralen Server nicht

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

KennzahlVorherNachherVerbesserung
Erstbesuch Antwortzeit2800 ms300 ms (erster Batch)89 % ↓
Cache-Hit Antwortzeit50 ms98 % ↓
JSON-Größe2,3 MB180 KB92 % ↓
Time-to-Interactive2800 ms300 ms89 % ↓
Serverlast100 %10 %90 % ↓

Kein „Aus welchem Jahrzehnt?“ mehr.

Performance-Monitoring und kontinuierliche Optimierung

Optimierung ist kein Endpunkt – ohne Monitoring keine Validierung.

Wichtige Kennzahlen

  1. Antwortzeit-Verteilung (P50, P95, P99)

    • P50: Median-Erfahrung
    • P95: 95 % der Nutzer
    • P99: langsamste 1 % (oft Anomalien)
  2. Cache-Hit-Rate

    • <70 %: Strategie prüfen
    • >95 %: evtl. zu lange Cache-Zeit, Daten veralten
  3. Fehlerrate

    • Nach Optimierung nicht steigen lassen
    • Streaming kann mittendrin abbrechen – besonders beachten
  4. 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

  1. Cache-Strategie regelmäßig prüfen – Business ändert sich, Cache muss mit
  2. A/B-Tests – unsicher? Testen
  3. 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?
Drei Invalidierungswege: 1) Zeitbasiert (revalidate-Zeit abgelaufen), 2) Manuell (revalidateTag oder revalidatePath aufrufen), 3) Erzwungenes Neuladen durch den Nutzer (Strg+Umschalt+R). Am häufigsten die ersten beiden – revalidate-Zeit passend zur Datenaktualisierungsfrequenz wählen.
Eignen sich Streaming-Responses für alle Endpoints?
Nein. Streaming passt bei großen Datenmengen (lange Listen) oder rechenintensiven Aufgaben (KI-Generierung). Bei kleinen, schnellen Daten reicht eine klassische Response – unnötige Komplexität vermeiden. Faustregel: Streaming ab >1 Sekunde Antwortzeit oder JSON >500 KB.
Welche Einschränkungen haben Edge Functions?
Drei Hauptlimits: 1) Keine Node.js-spezifischen APIs (fs, child_process), 2) DB-Verbindungen nur HTTP-basiert (z. B. Prisma Data Proxy), 3) Speicher max. 128 MB, Laufzeit max. 30 Sekunden. Gut für Auth und Proxy – komplexe Logik bleibt im Node.js Runtime.
Wie wählt man die richtige Caching-Strategie?
Nach Aktualisierungsfrequenz: Statische Daten (Config, Kategorien) lang cachen (1 Stunde+), Nutzerdaten (Profile) mit stale-while-revalidate (60 s frisch + 300 s Hintergrund-Update), Echtzeitdaten (Aktienkurse) nicht cachen oder WebSocket. Merken: Längerer Cache = bessere Performance, aber potenziell veraltete Daten.
Wie validiert man Optimierungsergebnisse?
Vier Kennzahlen: 1) Antwortzeit (P50, P95, P99), 2) Cache-Hit-Rate (Ziel 70–95 %), 3) Fehlerrate (darf nach Optimierung nicht steigen), 4) Latenz nach Region. Vercel Analytics, Next.js Instrumentation API oder eigenes Logging. A/B-Test vor und nach der Optimierung.

11 Min. Lesezeit · Veröffentlicht am: 5. Jan. 2026 · Aktualisiert am: 14. Juli 2026

Ähnliche Beiträge

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog