Reduzierung der Storyblok-API-Nutzung um 90 Prozent in deiner Next.js-App
Storyblok ist ein beliebtes Headless-CMS, das durch die Einbettung deines Frontends in einen Live-Editor ein hervorragendes Nutzererlebnis bietet. So können Redakteur:innen ihre Änderungen direkt im Kontext sehen, während sie Inhalte bearbeiten. Allerdings: Wenn du Storyblok gemäß der offiziellen Dokumentation in deine Next.js-App integrierst, kann das schnell zu einer extrem hohen API-Nutzung führen – denn standardmäßig ist kein effektives Caching eingerichtet. In diesem Artikel zeigen wir, wie wir die API-Nutzung bei einem unserer Kunden um 90 Prozent reduziert haben und wie du das ebenfalls erreichen kannst.
Bevor wir ins Detail gehen, möchte ich dir zuerst den Beweis liefern, dass unser Ansatz wirklich funktioniert.
)
Storyblok API-Nutzung im Laufe der Zeit
In der Grafik sehen wir, dass der Kunde eine sehr stark frequentierte Website betreibt, mit über 20 Millionen API-Anfragen pro Monat. Doch ab Freitag, dem 27. Juni, ist eine deutliche Veränderung zu erkennen: An diesem Tag haben wir eine subtile, aber wirkungsvolle Änderung am Code vorgenommen: Caching für Storyblok-Daten wurde eingeführt. Diese Optimierung hat die Anzahl der Anfragen an die Storyblok-API erheblich reduziert. Wichtig dabei: Die Analytics-Tools zeigen, dass die tatsächliche Nutzung der Website unverändert hoch geblieben ist. Das bestätigt, dass sich die Optimierung nicht negativ auf das Nutzererlebnis ausgewirkt hat.
Abschätzung einer unteren Grenze für API-Anfragen
Um zu verstehen, wie viele API-Anfragen mindestens notwendig sind, um Inhalte aktuell zu halten, wollen wir eine untere Grenze bestimmen.
Angenommen, es gibt N Stories in einem Storyblok-Space und M Änderungen an beliebigen Stories pro Monat. Auf den ersten Blick könnte man annehmen, dass M Anfragen ausreichen, eine pro Änderung. Aufgrund von Abhängigkeiten (z. B. Stories, die auf andere Stories, Assets oder Datenquellen verweisen), kann jedoch eine einzige Änderung dazu führen, dass mehrere Stories neu geladen werden müssen.
Um auf Nummer sicher zu gehen, nehmen wir an, dass jede der M Änderungen erfordert, dass alle N Stories neu geladen werden. Ein Beispiel: Wenn es 300
Stories gibt und wir durchschnittlich 14
Änderungen pro Woche haben (etwa 2
pro Tag), dann ergibt sich über einen Monat (ca. 4,3
Wochen) folgende Anzahl an Anfragen:
300 × 14 × 4,3 = 18.060 Anfragen/Monat
Diese konservative Schätzung liegt deutlich unter dem Limit des kleinsten Storyblok-Plans, der bis zu 100.000 API-Anfragen pro Monat erlaubt.
Das Problem: In-Memory-Caching als Standard
Wenn du das Storyblok SDK gemäß dem offiziellen Tutorial einrichtest, nutzt du standardmäßig das eingebaute In-Memory-Caching von Storyblok. Wenn der Client eine Story bereits abgerufen hat, wird sie nicht erneut angefragt, es sei denn, es wurde eine neue Cache-Version über eine andere API-Anfrage erkannt.
Das Problem? Dieses Caching passiert vollständig im Arbeitsspeicher (In-Memory). Das funktioniert vielleicht noch, wenn du nur eine einzige Instanz deiner App betreibst (auch wenn ein Neustart der App den Cache komplett löscht). In heutigen Deployment-Umgebungen ist dieses Szenario jedoch die Ausnahme.
Gerade bei Next.js-Anwendungen erfolgt das Deployment häufig in einem "serverless" Kontext, bei dem du keinerlei Kontrolle darüber hast, wie viele Instanzen deiner App gleichzeitig laufen.
Plattformen wie Vercel abstrahieren die Infrastruktur komplett. Deine App kann dynamisch skalieren: Zu Stoßzeiten können 100 Instanzen laufen, zu ruhigeren Zeiten vielleicht gar keine. In einem solchen Umfeld ist ein instanzgebundener In-Memory-Cache praktisch nutzlos. Jede neue Instanz startet mit einem leeren Cache, was zu unnötig vielen Anfragen an Storyblok führt.
Caching – eines der schwersten Probleme der Informatik (außer man nutzt Next.js)
Okay, das stimmt nicht ganz. Wenn ein einziges Feature eine 4.000 Wörter lange Dokumentation benötigt, weiß man: Caching bleibt eine Herausforderung.
Aber: Wenn man Caching in Next.js richtig konfiguriert, sind die Vorteile deutlich spürbar: Weniger Anfragen an externe APIs, kürzere Ladezeiten für Nutzer:innen und geringere Serverlast – was sich letztlich auch positiv auf die Cloud-Kosten auswirken kann.
Für den Moment konzentrieren wir uns auf ein Ziel: die Anzahl der Anfragen an Storyblok zu reduzieren.
Dafür müssen wir den Next.js Data Cache nutzen. Damit lassen sich einmal abgerufene API-Antworten instanzübergreifend und sogar deploymentübergreifend speichern.
Der einfachste Weg, das zu erreichen, ist über die integrierte fetch
-Funktion. Next.js erweitert diese Funktion um einige nützliche Features (auch wenn das Patchen von Built-ins grundsätzlich fragwürdig ist).
Hier sind zwei Beispiele:
const url = "...";
const result1 = fetch(url, {});
const result2 = fetch(url, { next: { revalidate: false } })
Du siehst, dass wir die next
-Konfigurationsoption an den fetch
-Aufruf übergeben haben. Das signalisiert Next.js, dass dieser Aufruf gecached werden kann! Natürlich kannst du in diesem Konfigurationsobjekt auch Dinge wie Revalidierungszeiten einstellen, so wie es in der Dokumentation beschrieben ist. Durch diese Optionen kannst du sicherstellen, dass Inhalte niemals erneut abgefragt werden, solange du weißt, dass sie sich nicht geändert haben.
Verbesserung des Caching-Verhaltens von Storyblok in Next.js
Jetzt müssen wir ändern, wie Storyblok seine Daten von der API abruft. Glücklicherweise können wir dem SDK einfach eine eigene fetch
-Funktion übergeben, in der wir die Parameter so anpassen, dass Caching aktiviert wird.
Wir halten es erstmal simpel: Storyblok-Inhalte sollen nie erneut geladen werden, wenn sie schon einmal abgerufen wurden:
const initResult = storyblokInit({
accessToken: environment.storyblok.accessToken,
use: [apiPlugin],
apiOptions: {
fetch: async (urlOrRequest) => {
const url = new URL(
typeof urlOrRequest === "string" || urlOrRequest instanceof URL
? urlOrRequest
: urlOrRequest.url,
);
// Make sure that the URL is stable and never changes
url.searchParams.delete("cv");
return fetch(url.toString(), {
next: { revalidate: false },
});
},
// If we don't set this, the storyblok api client will internally maintain a rate limit
// we don't want this, since we are using the next.js cache.
rateLimit: 100000,
cache: { type: "none" },
},
});
Drei wichtige Punkte, die du beachten solltest:
- Wir entfernen den
cv
-Parameter aus der URL, sodass sich diese nicht verändert. Dadurch kann Next.js den Aufruf dauerhaft cachen, da die vollständige URL Teil des Cache-Keys beifetch
-Anfragen ist. - Wir setzen
revalidate
auffalse
, was Next.js anweist, nie erneut Daten von der Quelle abzurufen, solange sie im Cache vorhanden sind. - Wir deaktivieren das integrierte Caching von Storyblok, da zwei separate Caching-Layer die Komplexität unnötig erhöhen.
Ein weiteres Problem muss noch gelöst werden:
Natürlich ändert sich CMS-Content im Laufe der Zeit, daher müssen Stories neu abgerufen werden, wenn es Änderungen gibt. Dafür können wir die Cache-Version-Funktion von Storyblok nutzen.
Lass uns also den obigen Code so anpassen, dass er regelmäßig prüft, ob bei Storyblok eine neue Cache-Version verfügbar ist:
// Retrieve the current cache version
async function getCv() {
return await storyblokClient
.get("cdn/spaces/me", { version: "published" })
.then((x) => x.data.space.version as number)
}
const storyblokSpaceCacheTag = "storyblok-space";
const initResult = storyblokInit({
accessToken: environment.storyblok.accessToken,
use: [apiPlugin],
apiOptions: {
fetch: async (urlOrRequest) => {
const url = new URL(
typeof urlOrRequest === "string" || urlOrRequest instanceof URL
? urlOrRequest
: urlOrRequest.url,
);
if (url.pathname === "/v2/cdn/spaces/me") {
/**
* Never put the cache verison in the request url for the space,
* as we use this request to get the up to date cache version.
*/
url.searchParams.delete("cv");
return fetch(url.toString(), {
next: {
tags: [storyblokSpaceCacheTag],
revalidate: 60,
},
});
}
url.searchParams.set("cv", (await getCv()).toString());
return fetch(url.toString(), {
next: { revalidate: false },
});
},
// If we don't set this, the storyblok api client will internally maintain a rate limit
// we don't want this, since we are using the next.js cache.
rateLimit: 100000,
cache: { type: "none" },
},
});
Für jede Anfrage holen wir jetzt zunächst die aktuelle Cache-Version von Storyblok. Das erzeugt eine zusätzliche Anfrage, die jedoch dank der Option revalidate: 60
im fetch
-Aufruf für den Storyblok-Space selbst gecached wird.
Diese aktualisierte Cache-Version wird anschließend in die URL eingefügt, mit der der eigentliche Story-Content abgerufen wird. Solange sich die Cache-Version nicht ändert, bleibt die URL gleich, und damit auch der Cache-Key unverändert. Das ermöglicht es Next.js, die gecachte Version der Story konstant wiederzuverwenden.
In der Beispielkonfiguration wird die Cache-Version alle 60 Sekunden aktualisiert. Alternativ kannst du revalidate
auf false
setzen und revalidateTag
verwenden, um den storyblokSpaceCacheTag
manuell über einen von Storyblok ausgelösten Webhook zu invalidieren.
Mit diesem Setup wird CMS-Content nur dann neu geladen, wenn sich tatsächlich etwas in deinem Storyblok-Space ändert.
Das bedeutet: Die Anzahl der API-Anfragen hängt ausschließlich von der Häufigkeit der Inhaltsänderungen ab – und nicht davon, wie oft Nutzer:innen deine Seite aufrufen. Ein wesentlich effizienteres Modell.
)
)
)
)
)
)
)
)
)
)
)
)