15 Punkte von GN⁺ 2025-06-02 | 1 Kommentare | Auf WhatsApp teilen
  • Wie bei progressivem JPEG werden auch JSON-Daten zunächst in unvollständigem Zustand übertragen, sodass der Client nach und nach den gesamten Inhalt nutzen kann
  • Das bestehende JSON-Parsing-Verfahren hat das Effizienzproblem, dass vor dem vollständigen Empfang der gesamten Daten keinerlei Verarbeitung möglich ist
  • Mit einem Breadth-first-Ansatz werden Daten in mehrere Chunks (Teile) aufgeteilt; noch nicht vorbereitete Teile werden als Promise dargestellt und nach und nach aufgefüllt, sobald sie bereit sind, sodass der Client auch unvollständige Daten nutzen kann
  • Dieses Konzept ist die zentrale Innovation von React Server Components (RSC), und mit <Suspense> werden beabsichtigte stufenweise Ladezustände gesteuert
  • Durch die Trennung von Daten-Streaming und bewusst gestaltetem UI-Ladefluss wird eine flexiblere User Experience möglich

Die Idee von progressivem JPEG und progressivem JSON

  • Ein progressives JPEG lädt ein Bild nicht auf einmal von oben nach unten, sondern zeigt zunächst die gesamte Ansicht unscharf und wird dann nach und nach schärfer
  • Ähnlich kann ein progressiver Ansatz auch auf die JSON-Übertragung angewendet werden, sodass Teildaten sofort nutzbar sind, ohne auf die vollständige Fertigstellung zu warten
  • Bei der beispielhaften JSON-Datenstruktur ist Parsing im üblichen Verfahren erst möglich, wenn wirklich das letzte Byte empfangen wurde
  • Dadurch muss der Client auch auf langsame Teile des Servers (z. B. das Laden von comments aus einer langsamen DB) warten, bis alles übertragen wurde, was den aktuellen Standard sehr ineffizient macht

Grenzen von Streaming-JSON-Parsern

  • Mit einem Streaming-JSON-Parser lässt sich ein unvollständiger (zwischenzeitlicher) Objektbaum erzeugen
  • Wenn jedoch Felder einzelner Objekte (z. B. footer oder mehrere comment-Listen) nur teilweise übertragen werden, entstehen Probleme wie Typinkonsistenzen und schwer erkennbare Vollständigkeit, was die Nutzbarkeit verringert
  • Ähnlich wie beim Streaming-Rendering von HTML tritt auch hier bei sequentieller Stream-Verarbeitung dasselbe Problem auf: Ein langsamer Teil verzögert das gesamte Ergebnis
  • Das ist ein Grund, warum Streaming-JSON im Allgemeinen selten verwendet wird

Vorschlag für eine Progressive-JSON-Struktur

  • Statt des bisherigen Depth-first-Streamings (also der Übertragung durch Traversierung bis in die tieferen Ebenen der Baumstruktur) wird ein Breadth-first-Ansatz eingeführt
  • Zunächst wird nur das Top-Level-Objekt übertragen; untergeordnete Werte bleiben als Platzhalter ähnlich Promises bestehen und werden jeweils als Chunks aufgefüllt, sobald sie bereit sind
  • Wenn der Server beispielsweise den asynchronen Ladevorgang eines Datenteils abgeschlossen hat, sendet er den entsprechenden Chunk, und der Client kann nur das nutzen, was bereits bereitsteht
  • Dadurch wird asynchroner Datenempfang (frühes Laden) möglich, ohne auf das Ende aller langsamen Teile warten zu müssen
  • Wenn der Client robust für nicht-sequenziellen und teilweise sequenziellen Empfang pro Chunk aufgebaut ist, kann der Server unterschiedliche Strategien zur Chunk-Aufteilung flexibel anwenden

Inlining und Outlining: effiziente Datenübertragung

  • Ein progressives JSON-Streaming-Format kann wiederverwendete Objekte (z. B. dieselbe userInfo, auf die an mehreren Stellen verwiesen wird) auch ohne doppelte Speicherung in einen separaten Chunk auslagern und an allen Positionen mit derselben Referenz nutzen
  • Nur langsame Teile werden als Platzhalter ausgelagert und übertragen, während der Rest sofort aufgefüllt wird, was einen effizienten Datenstrom ermöglicht
  • Wenn dasselbe Objekt mehrfach vorkommt, kann es nur einmal übertragen und wiederverwendet (Outlining) werden
  • Auf diese Weise lassen sich auch zyklische Referenzen (eine Struktur, in der ein Objekt auf sich selbst verweist) anders als in normalem JSON problemlos als indirekte Referenzstruktur zwischen Chunks natürlich serialisieren

Progressive-Streaming-Implementierung in React Server Components (RSC)

  • Reale React Server Components sind ein repräsentatives Beispiel für die Anwendung des progressiven JSON-Streaming-Modells
    • Der Server verwendet eine Struktur, in der externe Daten (z. B. Post, Comments) asynchron geladen werden
    • Auf Client-Seite werden noch nicht eingetroffene Teile als Promise behandelt und die UI wird entsprechend der Reihenfolge der Bereitstellung schrittweise gerendert
  • Mit Reacts <Suspense> werden bewusst gestaltete Ladezustände definiert
    • Um unnötige Sprünge in der UI aus Sicht der User Experience zu vermeiden, werden Promise-Zustände (Lücken) nicht sofort gezeigt; stattdessen lässt sich mit dem <Suspense>-Fallback ein stufenweiser Ladevorgang inszenieren
    • Selbst wenn Daten schnell eintreffen, kann der Entwickler steuern, dass die tatsächliche UI entsprechend der geplanten Stufen progressiv sichtbar wird

Zusammenfassung und Implikationen

  • Die zentrale Innovation von React Server Components besteht darin, Props im Komponentenbaum schrittweise von außen nach innen zu streamen
  • Daher muss nicht gewartet werden, bis der Server sämtliche Daten vollständig vorbereitet hat; stattdessen können wichtige Teile zuerst gezeigt und Ladezustände fein gesteuert werden
  • Nicht nur das Streaming selbst, sondern auch strukturelle Unterstützung wie das Programmiermodell, das es nutzt (z. B. Reacts <Suspense>), ist notwendig
  • So lassen sich Engpässe bisheriger Übertragungsverfahren abmildern, etwa das Problem, dass ein einzelner langsamer Datenteil alles verzögert

1 Kommentare

 
GN⁺ 2025-06-02
Hacker-News-Kommentare
  • Es wirkt, als würden manche Leute diesen Artikel zu wörtlich nehmen; zur Einordnung: Dan Abramov schlägt hier kein neues Format namens Progressive JSON vor
    • Der Artikel erklärt vielmehr die Idee hinter React Server Components und wie ein Komponentenbaum als JavaScript-Objekt dargestellt und dann als Stream übertragen werden kann
    • Dieser Ansatz erlaubt es, „Löcher“ im React-Komponentenbaum zu lassen, sodass zunächst ein Ladezustand oder eine Skeleton-UI angezeigt werden kann und dieser Teil vollständig gerendert wird, sobald die echten Daten vom Server eintreffen
    • So werden feiner abgestufte Ladeanzeigen und ein schnelleres First Paint möglich
  • Ich finde es eigentlich gut, wenn Leute diese Idee weiterdenken und auf andere Ansätze anwenden
    • Die Absicht war, die Serialisierung von RSC-Daten nicht nur auf React zu beschränken, sondern als allgemeineres Muster zu beschreiben
    • Es wäre schön, wenn verschiedene Ideen aus React Server Components auch in andere Technologien oder Ökosysteme einfließen würden
  • Mir gefällt Progressive Loading nicht besonders, vor allem die Erfahrung, wenn sich Inhalte ständig bewegen oder springen
    • Besonders störend finde ich das Muster, während des Ladens einen leeren State oder eine Placeholder-UI zu zeigen
  • Als ich vor einiger Zeit noch Ember genutzt habe, gab es etwas Ähnliches, und ich erinnere mich daran, wie schmerzhaft das Schreiben von Ajax-Endpunkten war
    • Offenbar ging es darum, durch Umordnung der Baumstruktur einige Kindelemente an das Dateiende zu verschieben, um DAGs (azyklische Graphen) effizienter zu verarbeiten
    • Mit einem Streaming-Parser im SAX-Stil kann man schon mit dem Rendern beginnen, wenn Daten nur teilweise angekommen sind
    • In einer Single-Thread-VM besteht aber die Gefahr, dass schlecht geplante Arbeitsreihenfolgen die Probleme eher vergrößern
  • Ich nutze Streaming Partial JSON (Progressive JSON) in der Praxis bereits bei der Anbindung an AI-Tools
    • Aus meiner Erfahrung ist das nicht nur für RSC nützlich, sondern an vielen Stellen ein praxisnaher und wertvoller Ansatz für Clients und Server gleichermaßen
  • Ich habe Dans Vortrag „2 computers“ und auch die jüngsten Postings zu RSC verfolgt
    • Dan ist einer der besten Erklärer im React-Ökosystem, aber wenn man eine Technik erst so kompliziert erklären muss, dann ist sie entweder
      1. wirklich unnötig oder
      2. die Abstraktion hat ein Problem
    • Die Mehrheit der Frontend-Entwickler versteht das RSC-Konzept noch immer nicht vollständig
    • Vercel hat Next.js zum Standard-Framework für React gemacht, und darüber verbreitet sich auch die RSC-Einführung
    • Selbst Leute, die Next.js verwenden, verstehen die Grenzen von Server Components oft nicht klar, und vieles wirkt wie eine Art Cargo-Cult-Adoption
    • Dass React keine Vite-bezogenen PRs angenommen hat, wirkt ebenfalls verdächtig. Der Push für RSC könnte in Wahrheit weniger für Nutzer oder Entwickler gedacht sein als für die Hosting-Plattform-Strategie von Plattformanbietern
    • Rückblickend wirkt auch die breite Übernahme des ursprünglichen React-Teams durch Vercel wie der Versuch, die Zukunft von React zu steuern
    • Dazu kam der Einwand, dass diese Einschätzung zu Motiven und Vorgeschichte falsch sei, verbunden mit einer Erklärung zum aktuellen Stand des Vite-Supports
    • Die Vite-Integration ist derzeit technisch dadurch eingeschränkt, dass in der DEV-Umgebung Bundling nötig ist; das Vite-Team arbeitet aber an Verbesserungen
    • Die Behauptung, Menschen würden RSC nicht verstehen, sei logisch zirkulär
    • Man kann RSC ablehnen, aber darin stecken trotzdem viele interessante Ideen, die sich auch in andere Technologien übernehmen lassen
    • Es geht weniger darum zu überzeugen, sondern eher darum, dass sich jeder die spannenden und nützlichen Teile herausnimmt
  • Natürlich kann man weiterhin SPAs als statische Sites bauen und auf ein CDN legen, und auch Next.js lässt sich im „dynamischen“ Modus selbst hosten
    • Allerdings ist es in der Praxis schwierig, den vollen Umfang des serverlosen Renderings von Next.js außerhalb von Vercel vollständig nachzubauen, auch wegen undokumentierter „Magie“
    • Auch hierfür wurde offiziell vorgeschlagen, Adapter einzuführen, um konsistente APIs über mehrere Plattformen hinweg anzubieten: https://github.com/vercel/next.js/discussions/77740
    • Ich glaube allerdings nicht, dass der RSC-Push primär aus Unternehmensinteressen kommt, sondern eher aus der Einsicht, dass klassische Website-Build-Muster (SSR plus etwas Progressive Enhancement auf dem Client) in der Praxis viele Vorteile haben
    • Schon bei reinem SSR gibt es das Problem, dass unnötig viel Business-Logik auf den Client wandert
  • RSC selbst ist technisch interessant, wirkt im realen Einsatz aber nicht besonders vernünftig
    • Es ist aufwendig, für das Rendern komplexer Komponenten in großem Maßstab Node-/Bun-Backend-Server zu betreiben
    • Eine statische Seite oder ein React-SPA plus Go-API-Server ist oft deutlich effizienter
    • Ähnliche Ergebnisse lassen sich mit viel weniger Ressourcen erzielen
  • Nur weil eine neue Technik kompliziert zu erklären ist, heißt das nicht automatisch, dass sie unnötig ist oder die falsche Abstraktion verwendet; es gibt Probleme, bei denen sich Komplexität lohnt
    • Ich beobachte erst einmal, wie sich diese Technik weiterentwickelt
  • Mit der Code-Struktur von RSC ließe sich vielleicht auch ein statischer Seitenbau umsetzen, bei dem HTML/CSS/JS in kleine Stücke zerlegt werden
    • Wenn man die im Artikel vorgeschlagenen „$1“-Platzhalter durch URIs ersetzt, braucht man womöglich gar keinen Server mehr; in den meisten Fällen ist dynamisches SSR nicht zwingend nötig
    • Der Nachteil ist, dass bei solchen Verfahren die Update-Pipeline bei Inhaltsänderungen schnell genug sein muss, besonders beim Streaming-Deployment kompilierter statischer Sites nach S3
    • Denkbar wäre etwa bei einer Nachrichtenwebsite mit vielen vorgerenderten Artikeln ein intelligentes Content-Diffing, sodass bei Teiländerungen nur die betroffenen Bereiche neu gebaut werden
  • In der Praxis wird oft angeblich auf Performance optimiert, indem im Frontend mehrere MB an Daten geladen und komplexe Logik im Millisekundenbereich ausgeführt wird, obwohl in Wirklichkeit BFF oder eine bessere Architektur und schlankere APIs viel produktivere Lösungen wären
    • Es gab Versuche mit GraphQL, http2 usw., aber das löst das eigentliche Problem nicht; ohne Weiterentwicklung der Webstandards wird es keinen echten Paradigmenwechsel geben
    • Auch neue Frameworks stoßen an dieselbe Grenze
  • RSC ist im Kern, wie auch am Ende des Artikels erklärt, im Wesentlichen eine BFF (Backend for Frontend)
  • Was „ein paar ms weniger Ladezeit“ bedeutet, hängt davon ab, was genau gemeint ist
    • Wenn man Time to first render oder time to visually complete optimieren will, wirkt es subjektiv am schnellsten, zuerst eine leere Skeleton-UI zu schicken und die Daten dann per API nachzuladen und zu hydratisieren
    • Wenn man dagegen time to first input oder time to interactive beschleunigen will, muss man Nutzerdaten direkt rendern können, und dafür ist das Backend meist klar im Vorteil, weil es Netzwerkanfragen minimiert
    • In den meisten Fällen bevorzugen Nutzer Letzteres; bei CRUD-SaaS-Apps passt serverseitiges Rendering gut, bei designlastigen Apps wie Figma eher ein Client mit statischen Daten plus zusätzlichem Daten-Fetching
    • Es gibt keine „eine Lösung für alle Probleme“, und der Optimierungspunkt ist letztlich eine subjektive Entscheidung
    • Auch Developer Experience, Teamstruktur und andere Faktoren beeinflussen die Technologiewahl
  • Dadurch verstehe ich jetzt, warum beim Laden von Facebook der Kerninhalt immer erst ganz am Ende gerendert wird
  • Es kam die Frage auf, was hier mit BFF gemeint ist
  • Wegen der vielen Abkürzungen fragte jemand auch, wofür FE und BFF stehen
  • Ich würde die Progressive-JSON-Idee nicht unbedingt direkt einsetzen; es gibt verschiedene Alternativen
    • Die einfachste Lösung wäre, ein riesiges JSON-Objekt in mehrere Teile aufzuteilen und als „JSON Lines“ zu übertragen
    • Header-Informationen einmal senden und große Arrays zeilenweise übertragen, um Stream-Verarbeitung effizienter zu machen
    • Wenn Objekte noch größer werden, kann man das rekursiv fortsetzen, allerdings wird es dann schnell zu komplex
    • Der Server könnte die Reihenfolge der Properties explizit garantieren, sodass Progressive Parsing und Trennung möglich werden
    • Für wirklich riesige Strukturen ist das am Ende vielleicht nicht ideal, aber für die häufigsten Fälle großer JSON-Datenmengen ist es ein ziemlich praktisches Werkzeug
  • Man muss „Löcher“ nicht explizit markieren; man kann auch Streaming-Nachrichten sequentiell senden und nur Deltas übertragen
    • Mit dem Delta-Format „Mendoza“ lassen sich Patches in Go und JS/Typescript sehr kompakt übertragen: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • Ähnlich wie bei zstd-Binärdeltas oder Mendoza speichert man nur Teile der serialisierten Daten im Speicher und kann so effizient patchen
    • Auch für React ist das ein sinnvoller Ansatz, weil dort ebenfalls Unterschiede verglichen oder nur Änderungen injiziert werden müssen
  • Beim Streaming von UI-Daten reichen leere Arrays oder null nicht aus; man braucht zusätzliche Informationen darüber, welche Daten gerade noch ausstehen
    • GraphQL-Streaming-Payloads wählen daher einen Mischansatz aus gültigem Datenschema, Pending-Informationen und späterer Patch-Verarbeitung
  • Wenn man weiß, welche Stellen „Löcher“ sind, lässt sich ein Ladezustand dort leichter anzeigen
  • Für Progressive Decoding von JSON auf dem Client wurde die Bibliothek jsonriver genannt: https://github.com/rictic/jsonriver
    • Sehr einfache API, gute Performance und ausreichend getestet
    • Sie parst gestreamte String-Fragmente schrittweise zu immer vollständigeren Werten
    • Das Endergebnis ist garantiert identisch zu JSON.parse
  • Für Baumdaten ist das eine interessante Technik, die sich auf jede Struktur anwenden lässt
    • Baumdaten könnte man als parent-, type- und data-Vektoren plus String-Tabelle darstellen; alles Weitere ließe sich auf wenige Ganzzahlen reduzieren
    • String-Tabelle und Typinformationen werden vorab als Header gesendet, parent- und data-Vektoren dann in Knoten-Chunks gestreamt
    • Für Depth-first- oder Breadth-first-Streaming genügt es, nur die Reihenfolge der Chunks zu ändern
    • Das könnte die Load-Time-UX von Anwendungen im Netzwerk deutlich verbessern
    • Man könnte Tabellen und Node-Chunks abwechselnd senden und den Baum in beliebiger Reihenfolge im Web visualisieren
    • Mit einer Preorder-Traversierung und Tiefeninformation ließe sich die Baumstruktur sogar ohne Node-ID rekonstruieren
    • Daraus eine kleine Bibliothek zu bauen, wäre einen Versuch wert
  • Die meisten Anwendungen brauchen solche „hochfeinen“ Ladesysteme gar nicht; oft reichen schlicht mehrere API-Aufrufe
    • Die Antwort darauf war, dass hier nur erklärt werden sollte, wie das RSC-Wire-Protokoll funktioniert, nicht dass irgendwer so etwas selbst implementieren soll
    • Das Verständnis der Prinzipien verschiedener Tools hilft aber dabei, Ideen an anderer Stelle zu übernehmen oder neu zu kombinieren
    • Mehrere Requests hätten zwar potenziell ein n*n+1-Problem, aber statt Objekte im OOP-/ORM-Stil tief verschachtelt zu übertragen, kann man Daten wie Kommentare auch flach senden
    • Dann haben typisierte Endpunkte mit protobuf und ähnlichen Formaten ebenfalls klare Vorteile
    • Wenn man Kommentare trennt, reichen zwei Requests aus (Seite+Beitrag und Kommentare separat), und so wird auch Pre-Render-Optimierung möglich
    • Wenn es gute, vordefinierte Tools gibt, muss man die tatsächliche Implementierungskomplexität nicht unnötig erhöhen oder tief anpassen
    • Man sollte sich bewusst sein, dass übermäßig komplexe Funktionen am Ende Nutzern oder Entwicklern schaden können
    • Andererseits könnte Progressive/Partial Reading in der WASM-Ära tatsächlich eine wichtige Rolle für die UX-Geschwindigkeit spielen, ähnlich wie man früher sagte, 640K seien genug
    • Wenn binäre Encodings wie protobuf Partial Reads und wohldefiniertes Streaming dazubekommen, steigt zwar die technische Last für Engineers, aber die UX könnte deutlich gewinnen
  • Progressive JPEGs sind wegen der Eigenschaften von Mediendateien sinnvoll, aber für Text/HTML eigentlich nicht nötig; dass wir das wegen riesiger JS-Bundles doch brauchen, wirkt wie ein selbstwidersprüchlicher Zustand
    • Die eigentliche Langsamkeit liegt aber nicht nur an der reinen Datengröße
    • Auch langsame Server-Queries oder träge Netzwerke können Progressive Reveal sinnvoll machen
    • Statt bis zur vollständigen Datenmenge zu warten, kann absichtlich gestuftes Rendern mit Lade-UI zum richtigen Zeitpunkt die User Experience tatsächlich verbessern
  • Die Strategie, Endpunkte aufzuteilen, hat ohnehin schon viele Vorteile, etwa gegen Head-of-Line-Blocking, für bessere Filteroptionen, Live-Updates oder unabhängige Performance-Optimierung
    • Das eigentliche Problem sei der Versuch, Anwendungen wie eine Dokumentplattform zu behandeln
    • Reale Anwendungen funktionieren nicht wie „Dokumente“, und um diese Diskrepanz zu überbrücken, braucht es dann viel zusätzlichen Code und Infrastruktur
    • Zu den echten Nachteilen separater Endpunkte und zur möglichen Weiterentwicklung wurden zwei längere Texte verlinkt: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • Kurz gesagt werden Endpunkte am Ende zu einem „offiziellen“ API-Vertrag zwischen Server und Client, und je stärker der Code modularisiert wird, desto eher drohen Performance-Nachteile wie Waterfalls
    • Entscheidungen serverseitig in einem Schritt zu bündeln (coalescing) kann für Performance und strukturelle Flexibilität die bessere Alternative sein