Einer der nützlichsten Produktionsfehler, die ich nach einem Deployment gesehen habe, wirkte zunächst fast belanglos:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
Er stand im selben Fehlerstrom wie Ausfälle von Analytics, Werbeskripten, generische Script error.-Meldungen und unterbrochene Videowiedergabe. Der größte Teil davon war Rauschen. Dieser Fehler war anders: Die fehlgeschlagene Ressource gehörte zu meiner eigenen Next.js-Anwendung. Wenn der Browser sie tatsächlich nicht laden konnte, konnte ein Teil der Seite ausfallen.
Was das Log nicht verriet, war die Ursache. Es konnte ein vorübergehendes Netzwerkproblem gewesen sein, ein Fehler in Proxy oder CDN, eine tatsächlich fehlende Datei oder eine alte Seite, die nach einem neuen Deployment noch einen Chunk der vorherigen Version anforderte.
Gerade diese letzte Variante wird leicht unterschätzt, weil das neue Deployment völlig gesund sein kann. Für jeden neuen Besucher funktioniert die aktuelle Version, während ein seit Stunden geöffneter Tab weiterhin ein Client der alten Version bleibt.
Darum geht es in diesem Artikel: weshalb alte Next.js-Tabs nach einem Deployment brechen können, wie veraltetes HTML und entfernte /_next/static-Assets Versionsversatz erzeugen, warum aggressives Aufräumen das Problem verschärft und wie ich Deployment, Aufbewahrung, Monitoring und Wiederherstellung gestalten würde, damit ein erfolgreiches Release bereits aktive Nutzer nicht zurücklässt.
Die erste Lektion: Nicht jeder Script-Fehler ist ein Deployment-Fehler
Der ursprüngliche Fehlerstrom enthielt völlig unterschiedliche Klassen von Problemen. Drittanbieter-Analytics und Werbung können durch Werbeblocker, DNS-Filter, Datenschutzfunktionen, regionale Filterung, Antivirensoftware oder das Netz des Nutzers blockiert werden. Ein play()-Promise eines Videos kann durch ein späteres pause() abgebrochen werden, ohne dass die Anwendung kaputt ist. Ein generisches, ursprungsübergreifendes Script error. liefert oft schlicht zu wenig Informationen.
Ein fehlgeschlagener eigener Next.js-Chunk verdient eine andere Priorität. Die nützliche Trennlinie ist nicht „JavaScript-Fehler oder kein JavaScript-Fehler“, sondern eher:
Drittanbieter-Ressource fehlgeschlagen
-> meist Telemetrie oder optionale Funktion betroffen
eigene /_next/static/*.js-Ressource fehlgeschlagen
-> Anwendungscode kann fehlen
Das ist wichtig, weil ein zu empfindlicher Reporter echte Störungen unter Hunderten irrelevanter Meldungen begraben kann. In meinem Fall war die entscheidende Ressource /_next/static/chunks/9253.647385b4be0958e4.js. Das Log belegt, dass ein eigener Script-Ladevorgang fehlschlug. Es belegt nicht, dass Versionsversatz nach einem Deployment die Ursache war.
Diese Grenze ist mir wichtig: Eine plausible Erklärung ist noch keine bestätigte Ursache.
Ein offener Browser-Tab ist praktisch ein Client eines älteren Releases
Das mentale Modell, das die Sache für mich verständlich machte, ist einfach: Nach einem Deployment können mehrere Versionen derselben Anwendung gleichzeitig weiterleben.
Angenommen, Release A ist um 10:00 Uhr aktiv. Ein Nutzer öffnet eine Seite und erhält HTML sowie den JavaScript-Code für diese Route. Um 10:30 Uhr wird Release B veröffentlicht. Neue Besucher bekommen B. Der bereits geöffnete Tab wird dadurch aber nicht automatisch zu B.
In diesem Tab können weiterhin stecken:
- die JavaScript-Laufzeit von Release A;
- Route- und Chunk-Referenzen, die A erzeugt hat;
- vorab geladene Navigationsdaten aus A;
- React-Zustand, der unter A entstanden ist;
- bereits heruntergeladene, aufgeteilte Module aus A;
- Verweise auf Module aus A, die noch nicht geladen wurden.
Der letzte Punkt macht das Problem sichtbar.
Wenn bereits alle jemals benötigten Dateien im Browser-Cache liegen, kann der Nutzer lange nichts bemerken. Next.js teilt Anwendungen jedoch in mehrere Code-Bestandteile auf. Ein Routenwechsel, ein dynamic import, ein Dialog, ein Editor oder eine erst später benutzte Funktion kann einen weiteren JavaScript-Chunk benötigen. Die alte Laufzeit fordert dann eine URL an, die für Release A gültig war.
Existiert diese Datei noch, funktioniert die Sitzung möglicherweise weiter. Wurde sie beim Deployment gelöscht, erhält der alte Client unter Umständen 404, obwohl Release B selbst vollständig gesund ist.
Chunks mit Inhalts-Hash sind absichtlich für langes Caching gebaut
Next.js versieht wirklich unveränderliche Assets bewusst mit langfristigen Cache-Headern. Die aktuelle Self-Hosting-Dokumentation erklärt, dass unveränderliche Assets mit SHA-Hash im Dateinamen mit einer einjährigen Policy ausgeliefert werden, zum Beispiel:
Cache-Control: public, max-age=31536000, immutable
Das ist sinnvoll: Ändert sich der Inhalt, ändert sich die URL. Der Browser muss diese Datei daher nicht bei jedem Abruf erneut validieren.
Daraus folgt aber etwas Wichtiges: Die alte URL bleibt relevant, solange noch ein altes Dokument oder eine alte Laufzeit darauf verweisen kann.
Ein Jahr Browser-Cache hilft nicht, wenn der Browser genau diesen Chunk vor dem Deployment noch nie geladen hat und ihn erst später anfordert, nachdem der Ursprung ihn bereits gelöscht hat.
„Unsere statischen Dateien sind unveränderlich“ ist deshalb nicht dasselbe wie „wir können die statischen Dateien des vorherigen Releases sofort löschen“. Unveränderlichkeit macht alte Assets sicher aufbewahrbar. Sie verhindert nicht, dass alte Clients sie noch anfordern.
Der aktuelle Next.js-Leitfaden zum Self-Hosting nennt fehlende JavaScript- oder CSS-Dateien ausdrücklich als mögliches Symptom von Versionsversatz bei mehreren Instanzen oder rollenden Deployments. Dasselbe Grundproblem kann zwischen einem alten Tab und einem bereits aktualisierten Ursprung auftreten.
Versionen können auf mehreren Wegen auseinanderlaufen
„Cache-Problem“ ist als Diagnose zu unscharf. Ich trenne mindestens vier Mechanismen, weil sie unterschiedliche Gegenmaßnahmen brauchen.
1. Ein alter Tab fordert ein bisher nicht geladenes Asset an
Das ist der klassische Fall einer langlebigen Sitzung. Dokument und Laufzeit stammen aus A. B ersetzt die Dateien auf dem Server. Später aktiviert der Nutzer eine Funktion, deren Lazy-Chunk noch nicht geladen war. Wenn dieser A-Chunk entfernt wurde, schlägt der Abruf fehl.
2. Veraltetes HTML verweist auf nicht mehr vorhandene Chunks
Ein CDN, Reverse Proxy, Service Worker, Browser-Cache oder eine statische Hosting-Schicht kann älteres HTML länger behalten als erwartet. Dieses HTML verweist auf Assets aus A, während der Ursprung nur noch B besitzt.
Besonders gefährlich wird es, wenn HTML versehentlich dieselbe lange immutable-Richtlinie wie gehashte Assets erhält. Der Chunk darf lange unverändert im Cache liegen, weil seine URL den Inhalt versioniert. HTML dagegen legt fest, welche Chunk-URLs zusammengehören.
3. Rollendes oder verteiltes Deployment mischt Releases
Stellen wir uns zwei Next.js-Instanzen hinter einem Load Balancer vor. Eine läuft bereits mit B, die andere noch mit A. Das Dokument kann von einer Version kommen, eine spätere Navigation von der anderen. Die aktuelle Next.js-Dokumentation bezeichnet das als Versionsversatz und nennt fehlende Assets, nicht passende Server Functions und Navigationsfehler als mögliche Folgen.
Eine sinnvolle Basis ist deshalb: einmal bauen und dasselbe Build-Artefakt auf allen Instanzen eines logischen Deployments starten. Die Next.js-Dokumentation empfiehlt ebenfalls, für dieselbe Bereitstellung dasselbe Build und eine konsistente Build-ID über Container hinweg zu verwenden, statt jede Instanz separat neu zu bauen.
4. Das Deployment veröffentlicht Dateien in der falschen Reihenfolge
Auch ohne alte Tabs kann ein nicht atomarer Upload vorübergehend einen unmöglichen Zustand erzeugen:
neues HTML ist sichtbar
+
neue Chunks sind noch nicht sichtbar
oder:
altes HTML ist noch sichtbar
+
alte Chunks wurden bereits gelöscht
Schon ein kurzes Zeitfenster reicht. Ein Nutzer muss nur einmal genau dort landen.
Gefährlich ist „alles ersetzen und den alten Baum löschen“
Ein einfaches Deployment-Skript beginnt häufig mit einem Muster wie:
build
rsync --delete new-output/ production/
restart
Das ist attraktiv, weil das Produktionsverzeichnis danach exakt dem neuesten Build entspricht. Für langlebige Clients ist es jedoch problematisch.
Bei gehashten Assets stören alte Dateien die neuen normalerweise nicht, weil ihre URLs verschieden sind. Sofortiges Löschen spart hauptsächlich Speicherplatz. Gleichzeitig wird aber jeder noch gültige Verweis eines alten Clients zu einem möglichen 404.
Ich betrachte alte Chunks deshalb als Kompatibilitätsmaterial eines Deployments und nicht als Abfall.
Das bedeutet nicht, jedes Release für immer aufzubewahren. Aufräumen sollte nur eine eigene Aufbewahrungsregel sein und kein Nebeneffekt des Umschaltens auf die neueste Version.
Aufbewahrung hilft, aber kein endliches Zeitfenster löst alles
Beim Self-Hosting kann man alte /_next/static-Assets für eine Schonfrist behalten. Wie lange, hängt vom Produkt ab. Eine Nachrichtenseite mit zweiminütigen Besuchen hat ein anderes Risikoprofil als eine Anwendung, die Nutzer den ganzen Arbeitstag geöffnet lassen.
Als Denkmodell eignet sich:
Aufbewahrungsfenster >=
erwartete Lebensdauer veralteten HTMLs
+ realistische Lebensdauer offener Tabs
+ Rollback-Fenster
+ Puffer für Deployment-Ausbreitung
Das ist keine mathematische Garantie. Ein Tab kann Wochen offen bleiben. Kein endliches Zeitfenster macht solche Fehler unmöglich.
Darum würde ich mehrere Schutzschichten kombinieren:
- alte unveränderliche Assets lange genug behalten, damit normale alte Sitzungen weiterlaufen;
- Versionsversatz erkennen und den Client auf das aktuelle Release bringen;
- eine kontrollierte, einmalige Neuladung oder einen sichtbaren Wiederherstellungsweg anbieten, wenn ein Asset wirklich fehlt;
- fehlende eigene Chunks überwachen und die Aufbewahrungsdauer an realen Daten ausrichten.
Die Aufbewahrung verhindert den Großteil der Fehler. Die Wiederherstellung deckt den langen Rest ab, den kein endliches Fenster vollständig beseitigt.
Alte Chunks nicht mit einer blinden Altersregel löschen
Auch „lösche alles, was älter als sieben Tage ist“ kann falsch sein. Ein aktuelles Release kann einen unveränderten, älteren gehashten Chunk wiederverwenden. Sein Änderungsdatum auf dem Datenträger ist dann alt, obwohl er weiterhin gebraucht wird.
Stärker ist eine releasebezogene Garbage Collection:
- Manifeste oder Asset-Listen aller Releases im Kompatibilitätsfenster behalten;
- die Vereinigungsmenge aller darin referenzierten Pfade bilden;
- nichts aus dieser geschützten Menge löschen;
- nur unreferenzierte Assets nach einer zusätzlichen Schonfrist entfernen.
Ist diese Logik für ein kleines System zu aufwendig, ist ein etwas größeres Static-Verzeichnis oft billiger als die Suche nach seltenen Produktionsfehlern.
Was ich vermeiden würde: --delete auf dem gemeinsamen /_next/static-Baum im selben Schritt auszuführen, der das neue Release produktiv schaltet.
Next.js besitzt heute Schutz gegen Versionsversatz, aber keine automatische Altdatei-Aufbewahrung
Aktuelles Next.js unterstützt deploymentId für den Umgang mit Versionsversatz. Eine Konfiguration kann so aussehen:
// next.config.js
const nextConfig = {
deploymentId: process.env.DEPLOYMENT_VERSION,
}
module.exports = nextConfig
Laut aktueller Dokumentation zu deploymentId hängt Next.js bei entsprechender Konfiguration den Parameter ?dpl=<deploymentId> an frameworkverwaltete URLs statischer Assets, übermittelt Deployment-Informationen bei clientseitigen Navigationsanfragen und signalisiert die eigene Deployment-ID in Serverantworten. Erkennt Next.js beim Navigieren eine Abweichung, kann es statt einer inkompatiblen Soft-Navigation eine vollständige Navigation auslösen.
?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-idDas ist wertvoll, aber die Funktion sollte nicht überschätzt werden. Die Dokumentation sagt ausdrücklich, dass Next.js den eingehenden Parameter ?dpl= nicht für versionsbezogenes Routing verwendet. Er dient dem Cache-Busting. Hat ein selbst gehosteter Ursprung die alte Datei physisch gelöscht, erzeugt die Query sie nicht neu.
Ich sehe deploymentId deshalb als Erkennung und Wiederherstellungsmechanismus, nicht als Ersatz für saubere Deployments und Asset-Aufbewahrung.
Hosting-Plattformen können weitergehen. Vercels aktuelle Skew-Protection-Dokumentation beschreibt zum Beispiel Versionsbindung, bei der frameworkverwaltete Anfragen weiter beim Deployment des ursprünglichen Clients landen. Das ist eine Plattformfunktion und keine Fähigkeit, die ich bei einer beliebigen Nginx- oder CDN-Konfiguration voraussetzen würde.
Build-ID und Deployment-ID sind verwandt, aber nicht identisch
Next.js erzeugt während next build außerdem eine Build-ID. Wenn mehrere Container dasselbe Deployment bedienen sollen, sollten sie nicht unbemerkt unterschiedliche Builds erzeugen, weil jeder Container separat gebaut wurde.
Eine deterministische Build-ID kann an eine Release-Kennung wie einen Git-Commit gekoppelt werden:
// next.config.js
const nextConfig = {
generateBuildId: async () => process.env.GIT_SHA,
deploymentId: process.env.GIT_SHA,
}
module.exports = nextConfig
Dieses Beispiel ist illustrativ und nicht aus meinem Produktionscode kopiert. Die Architekturregel ist wichtiger: Ein logisches Release sollte ein kohärentes Build-Artefakt und eine einheitliche Deployment-Identität über alle beteiligten Instanzen haben.
generateBuildId identifiziert das Next.js-Build. deploymentId ist ausdrücklich für Version-Skew-Schutz und Cache-Busting dokumentiert. Die Begriffe gleichzusetzen erschwert spätere Diagnose.
Assets sollten vor dem neuen Dokument veröffentlicht werden
Eine sichere Deployment-Reihenfolge ist bewusst asymmetrisch. Neue unveränderliche Assets dürfen existieren, bevor irgendjemand auf sie verweist. Neues HTML sollte nicht sichtbar werden, bevor seine Assets verfügbar sind.
Konzeptionell will ich diese Reihenfolge:
1. Version B genau einmal bauen
2. B-Ressourcen unter /_next/static hochladen
3. Erreichbarkeit der benötigten Ressourcen prüfen
4. Server/Laufzeit von B vorbereiten
5. Health-Check durchführen
6. neue Dokumentanfragen atomar auf B umschalten
7. statische Ressourcen von A weiter verfügbar halten
8. B überwachen
9. alte Ressourcen erst später bereinigen
Bei einem statischen Export gilt dasselbe: versionierte Assets zuerst hochladen, das darauf verweisende HTML danach. Bei SSR hinter einem Reverse Proxy wird der neue Server vorbereitet und geprüft, bevor Traffic umgeschaltet wird.
Auch Rollback sollte symmetrisch funktionieren. Ein vorhandenes vorheriges Release-Verzeichnis samt Assets macht Rückkehr möglich, ohne alte Dateien rekonstruieren zu müssen.
Das löst nicht jede Rollback-Frage. Eine inkompatible Datenbankmigration oder API-Änderung kann das alte Programm unbrauchbar machen, obwohl sein JavaScript noch vorhanden ist. Asset-Aufbewahrung löst nur den statischen Teil der Release-Kompatibilität.
Ein gemeinsames unveränderliches Asset-Verzeichnis ist für einfaches Self-Hosting praktisch
Bei einer kleinen Nginx-Installation kann man die aktive Anwendungsversion von einem gemeinsamen statischen Speicher trennen.
Ein illustratives Layout:
/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/
/srv/app/shared/_next/static/...
Jedes Deployment fügt seine neuen /_next/static-Dateien hinzu, ohne Dateien noch aufbewahrter Releases zu löschen. Nginx kann den Pfad mit einer unveränderlichen Cache-Richtlinie ausliefern:
location ^~ /_next/static/ {
root /srv/app/shared;
add_header Cache-Control "public, max-age=31536000, immutable";
}
Das ist ein Beispiel, keine Behauptung über meine konkrete Nginx-Konfiguration. In einer realen Umgebung müssen Berechtigungen, MIME-Typen, komprimierte Varianten, CDN-Verhalten und das genaue Next.js-Ausgabeformat berücksichtigt werden.
Entscheidend ist die Trennung der Lebenszyklen: Der Zeiger auf das aktuelle Release ist veränderlich, der Speicher für gehashte Assets ist überwiegend nur ergänzend.
HTML braucht eine andere Cache-Strategie als gehashte Chunks
Eine sehr einfache Möglichkeit, das Problem selbst zu erzeugen, besteht darin, HTML wie ein gehashtes, unveränderliches Asset zu behandeln.
Bei dynamisch gerenderten Next.js-Seiten verwendet das Framework üblicherweise nicht wiederverwendbare Cache-Semantik für nutzerspezifische dynamische Antworten. Statische und ISR-Seiten haben andere Regeln und können sinnvoll über ein CDN zwischengespeichert werden. Bei einem statischen Export hängt wiederum viel von den selbst gesetzten Server-Headern ab.
Darum denke ich nicht in einer einzigen Cache-Regel für „die Website“, sondern in Objektklassen:
gehashte /_next/static-Ressource
lange max-age
immutable
sicher aufzubewahren
HTML / Routendokument
muss auf die neue Version wechseln können
Richtlinie hängt vom Rendering-Modell ab
darf die referenzierten Ressourcen nicht überleben
RSC / Navigation / API-Daten
eigene Regeln für Frische und Kompatibilität
Mit CDN kann es je nach Architektur sinnvoll sein, Dokumentpfade nach einem Deployment zu invalidieren. Alte gehashte Chunks nur wegen eines neuen Releases zu purgen ist dagegen oft kontraproduktiv: Sind sie am Ursprung ebenfalls gelöscht, entfernt man womöglich die letzte Kopie, die einen alten Tab noch gerettet hätte.
Der Next.js-Leitfaden zum CDN-Caching ist hier nützlich, weil er Seiten-Caching klar von der langfristigen /_next/static-Richtlinie trennt.
Automatisches Neuladen ist ein Wiederherstellungswerkzeug, keine Deployment-Strategie
Auf einen Chunk-Fehler folgt oft der Rat: „Einfach Seite neu laden.“ Das funktioniert häufig, weil eine Hard-Navigation das aktuelle Dokument und damit die aktuelle Chunk-Liste abruft.
Ein pauschaler Reload bei jedem Script-Fehler schafft jedoch neue Probleme:
- ein fehlerhaftes Drittanbieter-Script löst sinnloses Neuladen aus;
- ein echter Serverausfall kann eine Endlosschleife verursachen;
- nicht gespeicherte Formulareingaben gehen verloren;
- lokaler React-Zustand verschwindet bei einer Hard-Navigation;
- ein wirklich defektes Release schlägt nach dem Reload erneut fehl.
Auch die aktuelle Next.js-Dokumentation weist darauf hin, dass eine Hard-Navigation bei Version-Skew-Wiederherstellung Komponentenstatus wie useState verliert, während URL-Zustand oder persistenter Browser-Speicher erhalten bleiben können.
Wenn ich clientseitige Wiederherstellung einbaue, soll sie eng begrenzt und einmalig sein. Ein illustratives Beispiel:
const RECOVERY_KEY = 'next-chunk-recovery-attempted'
function isOwnNextAsset(url: string) {
try {
const parsed = new URL(url, window.location.href)
return (
parsed.origin === window.location.origin &&
parsed.pathname.startsWith('/_next/static/')
)
} catch {
return false
}
}
window.addEventListener(
'error',
(event) => {
const target = event.target
if (!(target instanceof HTMLScriptElement)) return
if (!isOwnNextAsset(target.src)) return
reportChunkFailure({
page: window.location.href,
asset: target.src,
})
if (sessionStorage.getItem(RECOVERY_KEY)) return
sessionStorage.setItem(RECOVERY_KEY, '1')
window.location.reload()
},
true,
)
Das ist bewusst nur ein Beispiel. In Produktion müsste man auch CSS-Chunks, bekannte Framework-Fehlerformen, destruktive Reloads und das Zurücksetzen der Recovery-Markierung nach einem gesunden Start berücksichtigen.
Bei Editor, Checkout oder langem Formular würde ich gegebenenfalls lieber einen Hinweis „Neue Version verfügbar – Arbeit speichern und neu laden“ anzeigen als sofort zwangsweise neu zu laden.
Monitoring sollte erkennen lassen, ob tatsächlich Versionsversatz vorliegt
„Script konnte nicht geladen werden“ reicht nicht. Um gelöschte alte Chunks von zufälligen Netzwerkfehlern zu unterscheiden, brauche ich Deployment-Kontext.
Nützliche Felder sind:
- URL des fehlgeschlagenen Assets;
- aktuelle Seiten-URL;
- ob es sich um eine eigene Ressource handelt;
- die für den Client sichtbare Release- oder Deployment-ID;
- Browser und Betriebssystem;
navigator.onLinenur als schwaches Signal;- Zeit seit Seitenaufruf;
- Nähe des Fehlers zu einem Deployment;
- ob bereits ein Recovery-Versuch stattfand;
- HTTP-Status, wenn serverseitig sichtbar;
- welches Release Ursprung oder Proxy tatsächlich bedient hat.
Dann werden Muster aussagekräftig.
Wenn viele Nutzer in unterschiedlichen Netzen direkt nach einem Deployment alte gehashte URLs anfordern und der Ursprung 404 liefert, wird zu frühes Löschen eine starke Erklärung. Wenn nur ein Nutzer ohne HTTP-Antwort einen Netzwerkfehler sieht, ist Versionsversatz weit weniger sicher. Liefert der Chunk 200, aber HTML oder den falschen MIME-Typ, liegt eher ein Routing- oder Proxy-Problem vor.
Eigene Chunk-Fehler würde ich außerdem getrennt von Drittanbieterfehlern alarmieren. Genau das folgt unmittelbar aus meinen ursprünglichen Logs: Das wichtige Signal lag in viel Browserrauschen versteckt.
Der Reproduktionstest ist einfach – solange der alte Tab wirklich alt bleibt
Solche Fehler entgehen üblichen Release-Tests, weil Entwickler nach dem Deployment reflexartig neu laden. Damit zerstören sie genau den Zustand, der geprüft werden soll.
Ein besserer manueller Test:
- Release A ausrollen;
- einen produktionsnahen Tab mit aktiviertem Browser-Cache öffnen;
- nur einen Teil der Anwendung benutzen, sodass manche Routen oder Lazy-Funktionen noch nicht geladen sind;
- den Tab offen lassen;
- Release B deployen;
- den alten Tab nicht aktualisieren;
- eine Route oder dynamische Funktion öffnen, deren Code noch nicht geladen war;
- Network und Console beobachten;
- prüfen, ob alte Asset-URLs weiterhin 200 liefern;
- prüfen, ob Versionsversatz kontrolliert zu einer Hard-Navigation führt.
Diesen Test würde ich mit CDN, mit mehreren Instanzen während eines Rolling Deployments und nach Ablauf des Aufbewahrungsfensters wiederholen.
Eine subtile Falle ist „Disable cache“ in DevTools. Für manche Diagnosen ist das hilfreich, aber es verändert das Verhalten. Der Long-Lived-Tab-Fall sollte auch mit realistischem Caching getestet werden.
Nicht jeder Chunk-Fehler wird durch alte Dateien gelöst
Asset-Aufbewahrung ist nützlich, weil sie einen konkreten Mechanismus behebt. Sie sollte nicht zur neuen Universalerklärung werden.
Ein eigener Chunk kann auch scheitern, weil:
- die Anfrage den Server nie erreicht;
- die Verbindung abbricht;
- eine Browser-Erweiterung blockiert;
- ein CDN-Edge vorübergehend ausfällt;
- Nginx falsch routet;
- der Server statt JavaScript eine HTML-Fehlerseite liefert;
- Kompression oder
Content-Encodingbeschädigt ist; - Dateiberechtigungen falsch sind;
- ein Teil-Deployment den Chunk nie hochgeladen hat;
- der Chunk vorhanden war, aber zu früh entfernt wurde;
- Client und Server inkompatible Releases verwenden.
Statuscode und Zeitpunkt sind entscheidend. Wiederholte 404-Antworten auf alte gehashte URLs nach jedem Release erzählen eine andere Geschichte als ERR_CONNECTION_RESET in einem einzigen Mobilfunknetz.
Darum schreibe ich meinen ursprünglichen Vorfall nicht nachträglich zu „veraltetes HTML hat die Seite definitiv gebrochen“ um. Was ich tatsächlich beobachtet habe, war ein echter First-Party-Chunk-Fehler. Versionsversatz ist daraus als ernstzunehmendes Fehlermodell entstanden, nicht als bewiesene historische Ursache.
Ein sicheres Deployment betrachtet alte Clients als Teil des Release-Umfangs
Der tiefere Denkfehler besteht darin, ein Deployment als einzelnen Zeitpunkt zu sehen, an dem A sofort zu B wird.
Auf dem Server kann ein Symlink das suggerieren. Im Gesamtsystem existieren aber noch alte CDN-Objekte, alte Browser-Dokumente, möglicherweise beide Serverversionen während eines Rollouts und vielleicht erneut A nach einem Rollback.
Der reale Release-Umfang ist deshalb ein Zeitintervall.
Meine Regeln für Next.js-Deployments folgen inzwischen diesem Modell:
- Ein Build pro logischem Release. Replikate sollen nicht unbemerkt unterschiedliche Ausgaben erzeugen.
- Unveränderliche Assets veröffentlichen, bevor Dokumente auf sie verweisen.
- Alte gehashte Assets für ein bewusst gewähltes Kompatibilitätsfenster behalten.
- Veränderliches HTML nicht mit derselben Cache-Regel wie gehashte Chunks behandeln.
deploymentIdnutzen, wenn das Deployment-Modell Versionsversatz zulässt.- Plattformseitigen Skew-Schutz nur dort einplanen, wo wirklich versionsbewusst geroutet wird.
- Recovery einmalig und zustandsbewusst gestalten.
- Eigene Chunk-Fehler als separates Produktionssignal überwachen.
- Deployments mit offenem alten Tab testen.
- Alte Assets später bereinigen, nicht beim Umschalten.
Die Regel, die ich heute verwende
Ein grünes Build und eine gesunde frisch geladene Seite beweisen nicht, dass ein Deployment für Nutzer sicher ist, die schon vorher da waren.
Der alte Tab ist kein Abfall. Er ist ein echter Client, der ein echtes vorheriges Release ausführt.
Mit diesem Modell wurde der Chunk-Fehler viel weniger mysteriös. Ein Inhalts-Hash gibt einem Asset eine stabile Identität. Langes Caching macht diese Identität effizient. Das Deployment muss sie aber lange genug respektieren oder dem Client einen kontrollierten Weg auf die neue Version geben.
Ich muss nicht jedes alte Release für immer betreiben. Ich brauche ein System, das die Zeit übersteht, in der alte Clients und neue Server legitim nebeneinander existieren.
Darauf läuft mein Deployment-Vertrag heute hinaus: Neue Nutzer bekommen das neue Release, alte Nutzer verlieren nicht die Dateien, die ihre laufende Version noch anfordern kann, und verbleibender Versionsversatz endet in einem kontrollierten Recovery-Pfad statt in einer kaputten Seite.