Benchmark des Fehlerwegs: Next.js, Remix und veraltete Autorisierungsdaten
Eine Fallstudie dazu, wie ein Fehler mit veralteten Berechtigungen die Entscheidung zwischen Next.js und Remix neu ausrichtet, sowie dazu, wie Frameworks hinsichtlich Mutationen, Cache-Laufzeiten und Fehlersuche bewertet werden können.
Vergleiche von Frameworks messen in der Regel den idealen Ablauf: einen neuen Browser, vorbereitete Daten und eine Seite, die einmal lädt. Produktionsysteme werden an anderen Stellen bewertet – in den Momenten nach Datenänderungen. Diese Fallstudie folgt einem Team, das denselben authentifizierten Bildschirm in Next.js sowie in einer Remix-ähnlichen Architektur entwickelte, beobachtete, wie Next.js bei allen Leistungsuntersuchungen gewann, und anschließend seine Entscheidung aufgrund einer nicht entfernbaren, widerrufenen Berechtigung änderte. Am Ende erhalten Sie eine praktische Methode, um Rendering-Frameworks anhand der Fehler zu bewerten, die Ihr Produkt tatsächlich nicht verkraften kann.
Eindeutiger Leistungssieger
Der Leistungsvergleich war nicht knapp. Die Next.js-Version schickte weniger JavaScript an den Browser, zeigte die erste nutzbare Ansicht schneller an, ließ aufwändige Datenverarbeitungen auf dem Server stattfinden und sorgte dank Caching dafür, dass wiederholte Navigationen sofortig erschienen.
Dann wurde ein Administrator aus einem Kundenkonto entfernt. Sowohl die Datenbank als auch der Audit-Trail zeigten diese Entfernung an, und beim Neuladen der Seite wurden die richtigen Berechtigungen angezeigt. Doch als der betroffene Benutzer über die In-App-Navigation wieder zur Kontoansicht zurückkehrte, waren die Admin-Funktionen erneut vorhanden. Schaltflächen, die eigentlich verschwinden sollten, waren wieder sichtbar.
Die Backend-API lehnte jede privilegierte Aktion ab, sodass sich keine Daten änderten und es zu keinem Sicherheitsvorfall kam. Genau diese Serverseitige Überprüfung ist der Grund, warum die Autorisierung immer direkt in der API durchgesetzt werden muss – niemals aus dem, was die Benutzeroberfläche anzeigt, abgeleitet werden darf. Doch diese Absicherung rettete die Architekturentscheidung nicht.
In der Remix-basierten Version fanden die Ingenieure innerhalb weniger Minuten alle Stellen, an denen veraltete Daten überleben konnten. Bei der Next.js-Version brauchte es mehrere Personen, zwei widersprüchliche Spuren sowie einen versehentlichen Hard-Reload, um herauszufinden, welche Schicht die Antwort erzeugt hatte. Diese Untersuchung änderte das Urteil.
Der Bildschirm, der zweimal erstellt wurde
Die betreffende Seite war kein Spielzeug. Sie diente vielen Nutzern und ermöglichte es den Administratoren, Personen, ihre Rollen, API-Schlüssel zu verwalten, festzulegen, wer auf die Rechnungsdaten zugreifen darf, sowie einen Überblick über alle Vorgänge auf dem Konto zu erhalten. Der Leseverkehr dominierte, doch die kritischen Aktionen waren Schreibvorgänge: Entzug einer Rolle, Erneuerung eines Schlüssels, Entfernung eines Mitglieds oder Änderung der Berechtigungen bezüglich der Sichtbarkeit von Rechnungen.
Drei Anforderungen zogen in unterschiedliche Richtungen:
- Schnelle anfängliche Darstellung – auch bei Konten mit langer Aktivitätsgeschichte.
In Next.js wurden die meisten Inhalte der Seite durch Server Components dargestellt, wobei nur kleine Client-Components für interaktive Widgets verwendet wurden. Kontodaten, die von mehreren verschachtelten Layouts genutzt wurden, gingen über einen Cache, um deren wiederholte Abfrage zu vermeiden, und erfolgreiche Schreibvorgänge lösten eine Neuvollständigung aus.
Die Remix-Style-Version basierte auf route-spezifischen Ladevorgängen für Leseoperationen und Aktionen für Schreibvorgänge, wobei Standard-HTTP-Cache-Header nur dann hinzugefügt wurden, wenn eine sichere Haltbarkeit bekannt war. Sobald eine Aktion abgeschlossen war, wurden die Ladevorgänge für die betroffenen Routen erneut ausgeführt – nach einer Abfolge, die so einfach war, dass man sie auf eine Tafel skizzieren konnte. (Zum Zeitpunkt der Niederschrift wird dieses Lader-und-Aktion-Modell weiterhin im Framework-Modus von React Router verwendet, in den Remix integriert wurde; zu den Namenskonventionen siehe die aktuellen Dokumentationen.)
Sowohl Implementierungen waren solide, und keine von ihnen war darauf ausgelegt, zu versagen. Unter idealen Bedingungen gewann eindeutig Next.js, was den ersten Fehler im Test aufzeigte: Reales Traffic ist nicht auf ideale Bedingungen beschränkt.
Wo Server Components die Anfrage verkomplizieren
Die Serverkomponenten hielten, was sie versprachen. Leseschutzsektionen benötigten keinen Client-Code, das Datenabrufen fand direkt neben den Komponenten statt, die es nutzten, langsame Anzeigen wurden ohne Blockierung des gesamten Systems gestreamt, und stabile Kontodaten verhinderten wiederholte Anfragen an den Server.
Der Preis dafür war, dass eine einzelne sichtbare Seite nun aus mehreren unabhängigen Prozessen zusammengesetzt wurde:
- Arbeit, die auf eine einzige Serverdarstellung beschränkt ist.
- Daten, die durch Caching auf der Serverseite wiederverwendet werden.
- Routerzustände, die im Browser über verschiedene Benutzeraktionen hinweg beibehalten werden.
- Mehrere Serverinstanzen, jede mit ihrer eigenen Kopie der gecachten Werte.
- Invalidierung durch Schreibvorgänge.
- Jegliche Proxy- oder CDN-Dienste, die vor der Anwendung liegen.
Für den Benutzer war es unwichtig, welche Schicht den veralteten Wert enthielt – für die Person im Bereitschaftsdienst hingegen von großer Dringlichkeit. Eine Spur zeigte, dass die Datenbank den aktualisierten Rollenzustand zurückgab; eine zweite zeigte, dass die Berechtigungs-Oberfläche den vorherigen Zustand anzeigte. Keine der beiden Darstellungen war falsch: Jede zeichnete einen unterschiedlichen Anfragenablauf nach, der durch das System auf einem anderen Weg verlief.
Bei der lokalen Entwicklung trat das Problem nie auf. Ein einziger Prozess verwaltete den gesamten Cache, die Entwickler aktualisierten diesen nach jeder Änderung, und Testkonten folgten niemals der Abfolge eines echten Kunden, der die App vor der Änderung der Berechtigungen geöffnet hatte. Durch das Vermeiden von wiederholter Arbeit wurde zugleich jede einfache Möglichkeit beseitigt, herauszufinden, welche Schicht einen bestimmten Wert bereitstellte.
Das genaue Verhalten jeder Next.js-Cache-Schicht, wie beispielsweise die Dauer, über die der Client-Router vorab geladene Segmente speichert, hat sich zwischen den Major-Versionen geändert. Das ist eine Lektion: Wenn Ihre Korrektheit von diesen Standardeinstellungen abhängt, notieren Sie diese Abhängigkeit und überprüfen Sie sie bei jedem Upgrade.
Warum die erzwungene Neu laden die eigentliche Lösung war
Das Support-Team stellte fest, dass ein erzwungener Neuladen das Problem beheben konnte. Das deutet auf den Browser-Cache hin, doch der Browser erklärte nur einen kleinen Teil davon. Ein Neuladen sendete eine völlig neue Anfrage an den Server, und diese Anfrage erreichte in der Regel eine Instanz ohne die veraltete Kopie. Die Client-Navigation nutzte hingegen einen Pfad mit anderen Daten und einer anderen Invaliderungshistorie. Da die Seite nach jedem Neuladen korrekt aussah, verschwand der Fehler immer wieder, sobald jemand sie anschaute.
Die synthetische Überwachung zeigte erneut denselben Blindpunkt auf. Jede Überprüfung begann mit einer neuen Sitzung und bestätigte, dass ein Benutzer mit der neuen Rolle die richtige Seite sah. Niemand führte den Übergang von der alten zu der neuen Berechtigung innerhalb einer bestehenden Sitzung durch. Das Team überwachte die Zustände, während der Fehler zwischen den Zuständen existierte.
Mit diesem Erkenntnisstand wurde die Wiederholung des Fehlers zuverlässig:
- Öffnen Sie den Kontoscreen, sodass er von einer Anwendungsinstanz bereitgestellt wird.
- Ändern Sie die Rolle des Benutzers von einer anderen Sitzung aus.
- Gehen Sie über die Client-Navigation zurück zum Screen, ohne neu zu laden.
- Überprüfen Sie, ob die in der Benutzeroberfläche angezeigte Berechtigungsversion mit dem übereinstimmt, was der Dienst gespeichert hat.
In den Komponenten gab es keinen Fehler. Der in Cache gespeicherte Eintrag verhielt sich genau wie konfiguriert; die festgelegte Haltbarkeit war einfach nicht geeignet für Autorisierungsdaten. Zu bezeichnen, es handele sich um einen „veralteten Cache-Fehler“, deutet auf einen fehlerhaften Codeabschnitt hin. Der eigentliche Fehler bestand in einer ungenannten architektonischen Garantie: Innerhalb welcher Zeit muss ein widerrufenes Recht aus allem verschwinden, was der Benutzer sehen kann?
Die erste Lösung verlagerte den Ausfall fast auf die Datenbank
Die schnellste Abhilfe bestand darin, das Caching in der gesamten Kontosektion auszuschalten. Die veraltete Berechtigung verschwand, und die Last auf der Datenbank stieg sofort an.
Nestierte Layouts und Panels hatten überlappende Kontodaten angefordert. Durch das Caching wirkten diese doppelten Lesevorgänge kostenlos; durch dessen Entfernung verwandelte sich ein Berechtigungsproblem in zunehmende Wartezeiten im PostgreSQL-Verbindungspool, was andere Kontoseiten gefährdete.
Debatten über Frameworks überspringen in der Regel genau diesen Punkt. Ein Cache kann ein Risiko für die Korrektheit darstellen und gleichzeitig eine Tragfähigkeit bieten. Seine Deaktivierung stellt die Aktualität wieder her, indem sie eine Annahme beseitigt, die die Datenbank gesund hielt.
Die Wiederherstellung erfolgte in kleineren, sichereren Schritten:
- Die Berechtigungsdaten wurden pro Konto und pro Berechtigungsversion ungültig gemacht, anstatt global.
- Die Autorisierungsabfragen wurden auf Anfrageebene durchgeführt, sodass sie niemals länger bestanden als eine einzelne Anfrage.
- Doppelte Abfragen wurden beseitigt, bevor der nicht im Cache gespeicherte Pfad erweitert wurde.
- Ein kleiner Teil des Traffics wurde zunächst über das neue Verhalten geleitet, während die Wartezeit im Pool überwacht wurde.
- Jede Antwort enthielt ab nun einen kleinen Marker, der angab, wie sie generiert wurde, welche Instanz sie bereitgestellt hat, welche Datenversion verwendet wurde und ob ein Cache geantwortet hatte.
Dieser Marker veränderte spätere Untersuchungen. Dadurch konnte die Herkunft einer Antwort erfasst werden, ohne die Berechtigungsdaten selbst offenzulegen, und die Entwickler mussten nicht mehr erraten, welcher Ausführungspfad bei einem Neuladen hilfreich war. Es handelt sich um eine kostengünstige Erweiterung für jedes System mit mehreren Caching-Ebenen.
Warum die langsamere Architektur bei diesem Produkt gewann
Die Remix-ähnliche Build-Methode war nicht vor verteiltem Zustand gefeit. In Browsern und CDN-Systemen existieren Caches, Datenbanken führen Kopien aus, und jede ernsthafte Anwendung stößt letztendlich auf veraltete Daten, teilweise fehlgeschlagene Abläufe sowie Wiederholungsversuche.
Ihre Funktionalität war begrenzter. Das Trio aus Lader, Aktion und Neuvalidierung verschaffte dem Team ein kompaktes mentales Modell davon, was nach einer Schreiboperation geschieht. Nachdem eine Aktion eine Rolle geändert hatte, konnten die Ingenieure die Routen benennen, deren Daten aktualisiert werden mussten, und wussten, wo dies zu überprüfen war. Die Grenzen der Anfragen entsprachen den bereits von den Tracing-Tools genutzten Kriterien. Es lud einige Daten zweimal herunter und benötigte zusätzlichen Browser-Code für bestimmte Widgets, doch sein Fehlverhalten ließ sich schnell identifizieren.
Dies passt also zu diesem speziellen Produkt, macht es aber nicht universell einsetzbar. Auf einer Website, die von Inhalten dominiert wird, deren Daten selten ändern und bei der viel Arbeit mit dem Browser anfällt, bleibt die Next.js-Architektur die bessere Wahl; ihre Rendering- und Caching-Fähigkeiten können eine deutlich bessere Benutzererfahrung bieten. Für ein authentifiziertes Betriebswerkzeug ist eine widerrufene, aber weiter sichtbare Berechtigung teurer als der gemessene Leistungsunterschied. Nachvollziehbare Schreibvorgänge und vorhersehbarer Aktualitätsgrad waren wichtiger.
Der Benchmark lag nicht falsch. Er hat nur eine unvollständige Frage beantwortet.
Die Bewertung neu auf Fehler ausrichten
Die zweite Bewertung startete nicht mit einem sauberen Browser und aktualisierten Daten. Sie begann bei den Situationen, die der erste Benchmark ausgeblendet hatte:
- Eine Berechtigung ändert sich, während die Seite geöffnet bleibt. Zeigen alle Zugangswege zur Seite die Änderung innerhalb der vereinbarten Zeit an, und kann ein Techniker erkennen, welche Version auf dem Bildschirm angezeigt wird?
- Eine Anwendungsinstanz verschwindet mitten im Betrieb. Kann der Benutzer sicher erneut versuchen, und hinterlässt eine unvollständige Bearbeitung einen verwirrenden Client-Zustand?
- Das Caching wird bei hoher Nutzerzahl deaktiviert. Kann die Datenbank einer dringenden, notwendigen Lösung standhalten?
- Zwei Anwendungsversionen bedienen Nutzer während eines Deployments. Können beide die im Cache gespeicherten Daten sowie die Antwortmeldungen auf Änderungen verarbeiten?
- Der Support reproduziert einen Kundenprozess nach. Können sie herausfinden, wie die Seite dargestellt wurde, welche Cache-Instanz und -Version reagiert hat sowie welche Datenversion angezeigt wurde – alles ohne Debugging der Live-Server?
Next.js blieb auf mehreren wichtigen Pfaden schneller. Das Remix-ähnliche Modell war für dieses Team leichter zu verstehen, sobald diese Pfade nicht mehr normal funktionierten. Zu diesem Zeitpunkt verglich das Team die Systeme anstelle von Screenshots.
Entscheidung nach dem, was mit der Seite schiefgehen könnte
Die Eingrenzung auf „Next.js ist schnell, Remix ist einfach“ hält dem Einsatz in der Produktion nicht stand. Bevor Sie die Leistung messen, beantworten Sie diese Fragen:
- Welche Daten darf das Produkt veraltet liefern – und für wie lange?
- Beeinflussen Mutationen Geld, Berechtigungen, Lagerbestände oder nur die Darstellung?
- Wie viele Caching- und Rendering-Grenzen kann das Team aus einem einzigen Kundenbericht erkennen?
- Kann die Datenbank weiterhin funktionieren, wenn der Cache entfernt wird?
- Wie verhalten sich zwei bereitgestellte Versionen nebeneinander?
Messen Sie anschließend den „happy path“.
Das Team kam zu einer geteilten Entscheidung. Next.js blieb bei Seiten, auf denen Server Components echte Client-Arbeit reduzierten und jedes Datenstück eine klar definierte, sichere Lebensdauer hatte. Häufig mutierende Berechtigungsdaten wurden absichtlich eintönig gehalten: request-basiert und niemals im Cache gespeichert. Für den Operations-Bildschirm, bei dem die korrekte Darstellung von Änderungen wichtiger war als eine schnelle statische Bereitstellung, wurde der Remix-Ansatz gewählt. Wenn Sie für solche Bildschirme bei Next.js bleiben, bietet die auf Tags basierende Invalidation, wie in der use cache Directive und der tagbasierten Revalidierung beschrieben, eine genauere Kontrolle, und Autorisierungsprüfungen innerhalb jeder Server-Aktion bleiben unabhängig von der Anzeige im UI die Sicherheitsgarantie.
Haupterkenntnisse
- Ein Benchmark, der mit einer neuen Sitzung beginnt, kann keine Fehler erkennen, die zwischen verschiedenen Zuständen bestehen; prüfen Sie Transitions innerhalb langlebiger Sitzungen.
- Betrachten Sie Autorisierungsdaten als eigene Klasse: sie sollten auf Anfragen beschränkt sein, versioniert werden und explizit ungültig gemacht werden können.
- Ein Cache kann eine echte Belastung darstellen; planen Sie, wie Sie ihn entfernen, bevor ein Incident dies erzwingt.
- Machen Sie die Quelle jeder Antwort sichtbar, indem Sie einen leichten Marker für den Render-Modus, die Instanz, die Version sowie den Cache-Zustand verwenden.
- Wählen Sie das Framework, dessen Fehlermöglichkeiten Ihr Team schnell erklären kann. Eine Option bietet möglicherweise mehr Werkzeuge für Geschwindigkeit; die andere kann es erleichtern, fehlerhafte Seiten zu diagnostizieren. Für einige Produkte ist letztere Eigenschaft die eigentliche Anforderung.