15 points par GN⁺ 2025-06-02 | 1 commentaires | Partager sur WhatsApp
  • Comme avec un JPEG progressif, les données JSON peuvent aussi être envoyées d’abord dans un état incomplet afin que le client puisse exploiter progressivement l’ensemble du contenu
  • Le parsing JSON classique souffre d’une inefficacité : aucune opération n’est possible tant que toutes les données n’ont pas été entièrement reçues
  • Avec une approche en breadth-first, les données sont découpées en plusieurs chunks (fragments) ; les parties pas encore prêtes sont représentées par des Promise puis remplies progressivement dès qu’elles le sont, ce qui permet au client d’utiliser aussi des données incomplètes
  • Ce concept est l’innovation centrale des React Server Components (RSC), qui utilisent <Suspense> pour contrôler des états de chargement intentionnellement échelonnés
  • En séparant le streaming de données du flux intentionnel de chargement de l’UI, on peut offrir une expérience utilisateur plus flexible

L’idée du JPEG progressif et du JSON progressif

  • Un JPEG progressif, au lieu de charger l’image d’un seul coup de haut en bas, affiche d’abord l’ensemble de façon floue puis l’affine progressivement
  • De la même manière, appliquer une approche progressive à la transmission de JSON permet d’utiliser immédiatement une partie des données sans attendre que l’ensemble soit complet
  • Dans l’exemple de structure de données JSON, l’approche habituelle ne permet le parsing qu’après réception de jusqu’au dernier octet
  • Le client doit donc attendre que tout soit transmis, y compris les parties lentes du serveur (par exemple le chargement des commentaires depuis une base de données lente), ce qui constitue le standard actuel très inefficace

Les limites d’un parseur JSON en streaming

  • L’introduction d’un parseur JSON en streaming permet de créer un arbre d’objets de données incomplet (intermédiaire)
  • Mais lorsque seuls certains champs d’un objet (par ex. footer, plusieurs éléments d’une liste de comment, etc.) sont transmis, cela entraîne des incohérences de type et rend difficile de savoir ce qui est réellement complet, ce qui réduit fortement l’utilité
  • Comme pour le rendu HTML en streaming, traiter le flux dans l’ordre fait qu’une seule partie lente peut retarder l’ensemble du résultat
  • C’est l’une des raisons pour lesquelles le streaming JSON est en général peu utilisé

Proposition de structure pour un JSON progressif

  • Au lieu du streaming classique en profondeur, c’est-à-dire parcourir et transmettre l’intérieur de l’arbre jusqu’aux niveaux inférieurs, on adopte une approche breadth-first (en largeur)
  • Seul l’objet de plus haut niveau est envoyé d’abord ; les valeurs enfants sont laissées comme placeholders de type Promise, puis remplies au fur et à mesure dans des chunks séparés
  • Par exemple, à chaque fois que le serveur termine un chargement de données asynchrone, il envoie le chunk correspondant, et le client peut exploiter uniquement ce qui est prêt
  • Cela permet une réception asynchrone des données (chargement précoce), sans attendre que plusieurs parties lentes soient toutes terminées
  • Si le client est conçu pour bien gérer une réception de chunks hors séquence ou partiellement séquentielle, le serveur peut appliquer avec souplesse différentes stratégies de découpage en chunks

Inlining et Outlining : une transmission de données plus efficace

  • Un format de streaming JSON progressif peut aussi éviter de dupliquer les objets réutilisés (par ex. la même information userInfo référencée à plusieurs endroits) en les extrayant dans un seul chunk et en les référençant depuis plusieurs emplacements
  • On peut isoler uniquement les parties lentes et les envoyer comme placeholders, tout en remplissant immédiatement le reste pour obtenir un flux de données plus efficace
  • Lorsqu’un même objet apparaît plusieurs fois, il est possible de ne l’envoyer qu’une seule fois puis de le réutiliser (Outlining)
  • Avec cette approche, les références circulaires (une structure où un objet se référence lui-même) peuvent elles aussi être sérialisées naturellement via une structure de références indirectes entre chunks, sans les difficultés habituelles du JSON standard

Mise en œuvre du streaming progressif dans les React Server Components (RSC)

  • Les React Server Components sont en pratique un exemple représentatif d’application du modèle de streaming JSON progressif
    • Le serveur utilise une architecture qui charge des données externes (par ex. Post, Comments) de manière asynchrone
    • Côté client, les parties non encore arrivées sont représentées par des Promise, puis l’UI est rendue progressivement selon l’ordre de disponibilité
  • React permet de définir des états de chargement intentionnels avec <Suspense>
    • Pour éviter des sauts d’affichage inutiles du point de vue de l’expérience utilisateur, on n’expose pas immédiatement l’état Promise (les « trous ») et on peut orchestrer un chargement par étapes via le fallback de <Suspense>
    • Même si les données arrivent rapidement, le développeur peut contrôler l’exposition progressive de l’UI selon les étapes prévues par le design

Résumé et implications

  • L’innovation centrale des React Server Components réside dans le streaming progressif des propriétés (props) de l’arborescence de composants depuis l’extérieur vers l’intérieur
  • Il n’est donc pas nécessaire d’attendre que le serveur ait préparé toutes les données : on peut afficher progressivement les parties importantes en contrôlant finement les états d’attente
  • Au-delà du simple streaming, un support structurel est nécessaire, notamment via un modèle de programmation qui l’exploite, comme <Suspense> dans React
  • Cela permet d’atténuer les goulots d’étranglement des approches classiques, notamment lorsqu’une seule portion de données lente retarde tout le reste

1 commentaires

 
GN⁺ 2025-06-02
Avis Hacker News
  • Certaines personnes semblent prendre cet article trop littéralement, alors qu’il ne s’agit pas pour Dan Abramov de proposer un nouveau format appelé Progressive JSON
    • L’article explique plutôt l’idée des React Server Components et la manière de représenter un arbre de composants sous forme d’objets JavaScript puis de l’envoyer en flux
    • Cette approche permet de laisser des « trous » dans l’arbre de composants React, de montrer d’abord un état de chargement ou une interface skeleton, puis de rendre complètement cette partie quand les vraies données arrivent du serveur
    • Cela permet un affichage du chargement plus fin et un rendu initial plus rapide
  • À mon avis, ce n’est pas une mauvaise chose que les gens étendent cette idée et l’appliquent ailleurs
    • L’intention était d’expliquer la sérialisation des données de RSC non pas comme quelque chose de limité à React, mais comme un motif plus général
    • J’aimerais que plusieurs idées découvertes dans React Server Components se diffusent dans d’autres technologies et écosystèmes
  • Je n’aime pas vraiment le chargement progressif, surtout l’expérience où le contenu continue de bouger ou de sauter
    • Le modèle qui affiche une UI vide pendant le chargement me gêne particulièrement
  • Quand j’utilisais encore Ember il n’y a pas si longtemps, il y avait quelque chose de similaire, et j’ai le souvenir que l’écriture des endpoints Ajax était très pénible
    • Le but semblait être de réorganiser la structure de l’arbre pour placer certains enfants en fin de fichier afin de traiter plus efficacement un DAG (graphe acyclique)
    • Avec un parseur de streaming de type SAX, on peut commencer à peindre avant même que toutes les données soient arrivées
    • Mais dans une VM mono‑thread, si l’ordre des opérations est mal conçu, on risque surtout d’aggraver les problèmes
  • J’utilise déjà en pratique un flux de JSON partiel (Progressive JSON) dans mes connexions avec des outils d’IA
    • Mon expérience montre que cette approche n’est pas utile qu’aux RSC : elle a de la valeur côté client comme côté serveur dans de nombreux contextes
  • J’ai suivi la présentation de Dan sur les « 2 computers » et ses récents billets sur les RSC
    • Dan est le meilleur vulgarisateur de l’écosystème React, mais si une technologie doit être expliquée de manière aussi complexe, alors soit
      1. c’est une technologie dont on n’a pas réellement besoin, soit
      2. il y a un problème dans l’abstraction
    • La majorité des développeurs front-end ne comprend toujours pas complètement le concept des RSC
    • Vercel a fait de Next.js le framework React par défaut, et l’adoption des RSC s’est répandue dans ce sillage
    • Même parmi les utilisateurs de Next.js, beaucoup ne comprennent pas clairement la frontière des Server Components et l’adoptent presque par « cargo cult »
    • Le fait que React n’ait pas accepté la PR liée à Vite paraît aussi suspect. J’ai l’impression que la poussée autour des RSC ne sert pas vraiment les utilisateurs ou les développeurs, mais plutôt la stratégie de vente de plateformes d’hébergement des acteurs de la plateforme
    • En y repensant, le fait que Vercel ait recruté en masse l’équipe historique de React donne aussi l’impression d’une volonté de piloter l’avenir de React
    • Quelqu’un a fait remarquer que cette lecture des motivations et du contexte historique était erronée, puis a expliqué la situation actuelle du support de Vite
    • L’intégration Vite est actuellement limitée par une contrainte technique : le bundling reste nécessaire en environnement DEV, et l’équipe Vite travaille à l’amélioration
    • L’argument selon lequel les gens ne comprennent pas les RSC serait, selon certains, circulaire sur le plan logique
    • On peut ne pas aimer les RSC, mais elles contiennent tout de même des idées intéressantes à reprendre dans d’autres technologies
    • Plus que convaincre, l’idée serait que chacun en retienne les aspects étonnants et utiles
  • Bien sûr, on peut toujours construire une SPA comme un site statique et la mettre sur un CDN, et Next.js peut aussi être auto‑hébergé en mode « dynamic »
    • Mais dans la réalité, il reste difficile d’implémenter complètement ailleurs que chez Vercel toutes les capacités de rendu serverless de Next.js, à cause d’une part de « magie » non documentée
    • Une proposition officielle a d’ailleurs été faite pour introduire des adaptateurs afin d’offrir une API cohérente sur plusieurs plateformes : https://github.com/vercel/next.js/discussions/77740
    • Pour ma part, je pense que la poussée des RSC vient moins d’un intérêt purement économique que d’une redécouverte des avantages réels du modèle historique du web (SSR + un peu de progressive enhancement côté client)
    • Même avec le seul SSR, on voit déjà le problème d’une logique métier qui migre inutilement en masse vers le client
  • Les RSC sont intéressantes en tant que technologie, mais peu raisonnables en production à mon avis
    • Maintenir à grande échelle des serveurs backend Node/Bun pour rendre des composants complexes représente une vraie charge
    • Un assemblage pages statiques ou React SPA + serveur API Go est bien plus efficace
    • On peut obtenir un résultat similaire avec beaucoup moins de ressources
  • On ne peut pas forcément conclure qu’une technologie est inutile ou mal abstraite simplement parce que son explication est complexe ; certains problèmes valent la peine d’assumer cette complexité
    • J’attends de voir comment cette technologie va évoluer
  • Je pense aussi qu’on pourrait utiliser la structure de code des RSC pour construire des pages statiques en découpant HTML/CSS/JS en petits fragments
    • Le placeholder « $1 » proposé dans l’article pourrait être remplacé par une URI, sans nécessité de serveur dans certains cas (la plupart des SSR dynamiques ne sont pas indispensables)
    • L’inconvénient, c’est que quand le contenu change, il faut une pipeline de mise à jour rapide, surtout pour le déploiement en streaming d’un site statique compilé sur S3
    • Par exemple, sur un site de presse où beaucoup d’articles sont pré‑rendus, il faudrait une gestion intelligente des diff de contenu pour ne reconstruire efficacement que les parties modifiées
  • En pratique, on voit souvent des optimisations « performance » où le front-end charge plusieurs Mo de données et exécute une logique complexe au milliseconde près, alors qu’en réalité un BFF, une meilleure architecture ou une API plus Lean seraient des solutions bien plus productives
    • Il y a bien eu des tentatives via GraphQL, http2, etc., mais elles ne résolvaient pas vraiment le problème de fond ; sans évolution des standards du web, il n’y aura pas de vrai changement de paradigme
    • Les nouveaux frameworks restent soumis à la même limite
  • Les RSC, comme expliqué à la fin de l’article, jouent fondamentalement le rôle d’un BFF (Backend for Frontend)
    • C’est une façon de structurer la logique d’API sous forme de composants
    • Voir mon long billet : https://overreacted.io/jsx-over-the-wire/ (la référence au BFF se trouve au milieu de la première section)
  • Tout dépend de ce qu’on entend par « gagner quelques ms au chargement d’une page »
    • Si l’on optimise le time to first render et le time to visually complete, envoyer d’abord une UI skeleton vide puis hydrater avec les données reçues par API donne souvent la sensation de vitesse la plus forte
    • À l’inverse, si l’on veut améliorer le time to first input et le time to interactive, il faut pouvoir rendre immédiatement les données utilisateur, et dans ce cas le backend est bien plus avantageux, car il réduit les appels réseau
    • Dans la plupart des cas, c’est ce que préfèrent les utilisateurs ; pour une appli SaaS CRUD, le rendu côté serveur est adapté, tandis que pour une app comme Figma, très orientée design, le client avec données statiques + fetch complémentaire convient mieux
    • Il n’existe pas de « solution unique pour tous les problèmes », et le point d’optimisation reste un choix subjectif
    • L’expérience développeur, la structure de l’équipe et bien d’autres facteurs influencent aussi les choix techniques
  • Grâce à ça, je comprends enfin pourquoi, quand Facebook se charge, le contenu principal arrive toujours en dernier
  • Quelqu’un demande ce que signifie exactement BFF dans ce contexte
  • Une autre réaction dit qu’il y a trop d’acronymes et demande ce que veulent dire FE et BFF
  • Je n’ai pas spécialement envie d’utiliser moi‑même l’idée de Progressive JSON, et je pense qu’il existe plusieurs alternatives
    • La solution la plus simple consiste à découper un énorme objet JSON en plusieurs morceaux, autrement dit à l’envoyer en « JSON lines »
    • On envoie les informations d’en‑tête une seule fois, puis les grands tableaux ligne par ligne pour améliorer l’efficacité du traitement en flux
    • Si les objets deviennent encore plus gros, on peut appliquer cette méthode récursivement, même si cela peut vite devenir trop complexe
    • Le serveur peut aussi garantir explicitement l’ordre des propriétés, ce qui permet de séparer le progressive parsing
    • Au final, ce ne sera sans doute pas utile pour des structures vraiment gigantesques, mais pour les cas fréquents de gros JSON c’est un outil assez pratique
  • Sans marquer explicitement des trous, on peut aussi envoyer des messages de streaming séquentiels en ne transmettant que les deltas (diff)
    • Avec un format de delta comme « Mendoza », on peut transmettre des patchs (diffs) très compacts en Go et en JS/Typescript : https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • Comme avec les diff binaires de zstd ou Mendoza, on ne garde qu’une partie des données sérialisées en mémoire pour appliquer les patchs efficacement
    • React aussi a besoin de comparer les différences ou d’injecter uniquement les changements, donc c’est une approche pertinente
  • Pour le streaming de données d’interface, un tableau vide ou une valeur null ne suffisent pas ; il faut aussi une information distincte pour savoir quelles données sont encore en attente
    • Les payloads de streaming GraphQL choisissent donc une approche hybride : un schéma de données valide, l’information de données non encore arrivées, puis les patchs ultérieurs
  • Il faut savoir où se trouvent les « trous » pour afficher facilement les états de chargement
  • Pour décoder progressivement du JSON côté client, quelqu’un recommande la bibliothèque jsonriver : https://github.com/rictic/jsonriver
    • API très simple, bonnes performances et tests suffisants
    • Elle parse des fragments de chaîne streamés en valeurs de plus en plus complètes
    • Le résultat final est garanti identique à celui de JSON.parse
  • Si l’on parle de données arborescentes, c’est une approche amusante qui pourrait s’appliquer à n’importe quelle structure
    • On peut représenter les données en arbre avec des vecteurs parent, type, data et une table de chaînes, ce qui permet de réduire tout le reste à un petit nombre d’entiers
    • On envoie d’abord la table de chaînes et les informations de type dans l’en‑tête, puis on streame les chunks des vecteurs parent et data nœud par nœud
    • Le streaming en depth-first ou breadth-first ne demande finalement qu’un changement d’ordre des chunks
    • Cela pourrait nettement améliorer l’expérience de temps de chargement dans les applications réseau
    • En alternant l’envoi de tables et de chunks de nœuds, on peut visualiser l’arbre sur le web dans n’importe quel ordre
    • Avec un parcours preorder et des informations de profondeur, on peut même reconstruire l’arbre sans identifiant de nœud
    • Ce serait une bonne idée d’en faire une petite bibliothèque
  • La plupart des applications n’ont pas besoin d’un système de chargement aussi « sophistiqué » et, dans la majorité des cas, plusieurs appels d’API suffisent largement
    • Réponse : mon objectif était seulement d’expliquer le fonctionnement du protocole wire des RSC, pas de recommander à quiconque de l’implémenter lui‑même
    • Comprendre les principes derrière différents outils aide malgré tout à réutiliser ou remixer des idées ailleurs
    • Je pense que la stratégie à appels multiples pose un problème de type n*n+1, mais au lieu d’envoyer des objets imbriqués façon OOP/ORM, on peut aussi transmettre les données à plat, comme dans l’exemple des commentaires
    • Dans ce cas, on peut aussi voir les avantages d’endpoints plus clairement typés avec protobuf, par exemple
    • Si l’on sépare les commentaires, deux appels suffisent : page + article d’un côté, commentaires de l’autre ; cela permet aussi d’optimiser le pre-render
    • Si l’on dispose d’outils prêts à l’emploi bien conçus, il n’y a pas forcément besoin de sur‑personnaliser l’implémentation des options
    • Il faut garder à l’esprit que des fonctionnalités excessivement complexes peuvent finir par nuire aux utilisateurs comme aux développeurs
    • Un peu comme l’idée que 640K suffiraient à tout le monde, je pense que les lectures progressives/partielles peuvent vraiment jouer un grand rôle dans la vitesse perçue à l’ère du WASM
    • Si l’on ajoute des lectures partielles et un streaming bien défini à un encodage binaire comme protobuf, la charge pour les ingénieurs augmentera, mais l’expérience utilisateur pourrait progresser de façon significative
  • Le Progressive JPEG est utile pour les fichiers média, mais pas vraiment pour du texte/HTML ; le fait d’en arriver là à cause de bundles JS trop volumineux ressemble à une contradiction auto‑infligée
    • Cela rappelle que la lenteur ne vient pas seulement de la « taille » des données
    • Les requêtes serveur elles‑mêmes peuvent être longues, ou le réseau lent ; dans ces cas aussi, une révélation progressive du contenu peut avoir du sens
    • Au lieu d’attendre que l’ensemble des données soit complet, un rendu volontairement étagé avec une UI de chargement au bon moment peut réellement améliorer l’expérience utilisateur
  • La stratégie de séparation des endpoints apporte déjà de nombreux avantages : éviter le head-of-line blocking, améliorer les options de filtre, faciliter les mises à jour en direct, permettre des optimisations de performance indépendantes, etc.
    • Pour certains, le problème de fond vient de la tentative de traiter les applications comme une plateforme documentaire
    • Or une application réelle ne fonctionne pas comme un « document », et ce décalage oblige à ajouter beaucoup de code et d’infrastructure
    • Deux longs billets complètent l’explication sur les vrais inconvénients des endpoints séparés et sur la direction possible de l’évolution : https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • En résumé, les endpoints finissent par devenir un contrat d’API « officiel » entre serveur et client, et à mesure que le code se modularise, cela peut nuire aux performances, notamment via des effets de waterfall
    • Regrouper les décisions côté serveur en une seule étape (coalescing) peut constituer une meilleure alternative, à la fois pour les performances et pour la flexibilité structurelle