15 puntos por GN⁺ 2025-06-02 | 1 comentarios | Compartir por WhatsApp
  • Al igual que un JPEG progresivo, los datos JSON también pueden enviarse primero en un estado incompleto para que el cliente vaya aprovechando gradualmente todo el contenido
  • El método tradicional de parseo de JSON tiene el problema de la ineficiencia: no se puede hacer nada hasta que se reciba por completo toda la información
  • Con un enfoque breadth-first, los datos se dividen en varios chunks (partes); las secciones que aún no están listas se representan como Promise y se van completando progresivamente conforme se preparan, de modo que el cliente puede usar incluso datos incompletos
  • Este concepto es la innovación central de React Server Components (RSC), y con <Suspense> se controlan los estados de carga por etapas de forma intencional
  • Al separar el streaming de datos del flujo intencional de carga de la UI, se puede ofrecer una experiencia de usuario más flexible

La idea del JPEG progresivo y el JSON progresivo

  • Un JPEG progresivo, en lugar de cargar la imagen de arriba hacia abajo de una sola vez, primero muestra la imagen completa en un estado borroso y luego la va haciendo más nítida
  • De forma similar, al aplicar un enfoque progresivo al envío de JSON, es posible usar parte de los datos de inmediato sin esperar a que todo esté completo
  • En una estructura de datos JSON de ejemplo, el enfoque habitual solo permite hacer el parseo una vez recibidos todos los bytes hasta el último
  • Por eso, el cliente tiene que esperar a que todo se termine de enviar, incluso las partes lentas del servidor (por ejemplo, cargar comments desde una base de datos lenta), lo cual es un estándar actual muy ineficiente

Límites de los parsers de JSON en streaming

  • Si se introduce un parser de JSON en streaming, es posible generar un árbol de objetos de datos incompleto (intermedio)
  • Sin embargo, cuando los campos de cada objeto (por ejemplo, footer o una lista de varios comment) llegan solo de forma parcial, surgen problemas de incompatibilidad de tipos y dificultad para saber qué ya está completo, lo que reduce su utilidad
  • Al igual que con el renderizado HTML en streaming, procesar el stream en orden hace que una sola parte lenta retrase todo el resultado, exactamente el mismo problema
  • Esta es una de las razones por las que el JSON en streaming rara vez se aprovecha en la práctica

Propuesta de estructura para Progressive JSON

  • En vez del streaming tradicional en profundidad, es decir, enviar recorriendo internamente la estructura de árbol hasta sus niveles inferiores, se propone un enfoque breadth-first (por amplitud)
  • Primero se envía solo el objeto de nivel superior, y los valores inferiores se dejan como placeholders similares a Promise, para luego ir completándolos en chunks separados conforme estén listos
  • Por ejemplo, cada vez que el servidor termina de cargar datos de forma asíncrona, envía el chunk correspondiente, y el cliente puede usar únicamente lo que ya esté disponible
  • Esto permite recibir datos de forma asíncrona (carga temprana) sin tener que esperar a que varias partes lentas terminen de procesarse por completo
  • Si el cliente se diseña para tolerar bien la recepción no secuencial por chunks y la recepción parcialmente secuencial, el servidor puede aplicar con flexibilidad distintas estrategias de división en chunks

Inlining y Outlining: transmisión eficiente de datos

  • Un formato de streaming de JSON progresivo puede, incluso para objetos reutilizados (por ejemplo, referenciar el mismo userInfo en varios lugares), extraerlos aparte en un solo chunk sin duplicarlos y permitir la misma referencia desde cada posición
  • Al separar solo las partes lentas y enviarlas como placeholders, mientras el resto se completa de inmediato, se logra un flujo de datos más eficiente
  • Cuando el mismo objeto aparece varias veces, es posible enviarlo una sola vez y reutilizarlo (Outlining)
  • De esta manera, incluso las referencias circulares (estructuras en las que un objeto se referencia a sí mismo) pueden serializarse de forma natural como referencias indirectas entre chunks, sin las dificultades típicas del JSON convencional

Implementación de streaming progresivo en React Server Components (RSC)

  • Un ejemplo representativo de aplicación real de este modelo de streaming progresivo de JSON es React Server Components
    • El servidor usa una estructura en la que carga datos externos (por ejemplo, Post, Comments) de forma asíncrona
    • En el cliente, las partes que todavía no han llegado se mantienen como Promise, y la UI se renderiza progresivamente en el orden en que van estando listas
  • React usa <Suspense> para definir estados de carga intencionales
    • Para evitar saltos visuales innecesarios en la experiencia de usuario, no se muestra de inmediato el estado Promise (el hueco), sino que se puede escenificar una carga por etapas con el fallback de <Suspense>
    • Aunque los datos lleguen rápido, el desarrollador puede controlar que la UI real se vaya mostrando progresivamente según las etapas diseñadas

Resumen e implicaciones

  • La innovación central de React Server Components está en hacer streaming progresivo de las props del árbol de componentes desde la parte exterior hacia adentro
  • Por eso, no hace falta esperar a que el servidor tenga todos los datos completamente listos; se pueden mostrar antes las partes importantes y controlar con más detalle los estados de espera de carga
  • No solo hace falta el streaming en sí, sino también soporte estructural como un modelo de programación que lo aproveche (por ejemplo, <Suspense> de React)
  • Con ello, se pueden aliviar cuellos de botella de los métodos de transmisión tradicionales, como el problema de que una sola parte lenta retrase todo

1 comentarios

 
GN⁺ 2025-06-02
Comentarios de Hacker News
  • Parece que algunas personas están tomando este artículo demasiado literalmente; se aclara que Dan Abramov no está proponiendo un nuevo formato llamado Progressive JSON
    • El artículo explica la idea de React Server Components y el proceso de representar el árbol de componentes como objetos de JavaScript y luego transmitirlo en forma de stream
    • Este enfoque permite dejar “huecos” en el árbol de componentes de React para mostrar primero estados de carga o una skeleton UI, y renderizar completamente esas partes cuando lleguen los datos reales desde el servidor
    • Así se pueden mostrar indicadores de carga más granulares y una primera pantalla más rápida
  • En mi opinión, está bien que la gente extienda esta idea y la aplique de otras maneras
    • La intención era describir la serialización de datos de RSC no solo como algo limitado a React, sino como un patrón más general
    • Ojalá varias de las ideas encontradas en React Server Components se incorporen también en otras tecnologías o ecosistemas
  • No me gusta mucho la carga progresiva, especialmente la experiencia en la que el contenido sigue moviéndose o “saltando”
    • Me molesta en particular el patrón de mostrar una UI vacía mientras carga
  • Cuando usaba Ember hasta hace poco, había un enfoque parecido, y recuerdo que escribir endpoints de Ajax era muy doloroso
    • Supongo que la intención era reorganizar la estructura de árbol para que algunos elementos hijos quedaran al final del archivo y así procesar un DAG (grafo acíclico dirigido) con mayor eficiencia
    • Si usas un parser de streaming estilo SAX, puedes empezar a pintar antes cuando los datos llegan parcialmente
    • Pero en una VM de hilo único, si diseñas mal el orden del trabajo, el problema puede empeorar todavía más
  • Yo ya uso en la práctica un enfoque de partial JSON en streaming (Progressive JSON) en integraciones con herramientas de IA
    • Mi experiencia real es que esto sirve no solo para RSC, sino en muchos otros lugares, y que aporta valor práctico tanto al cliente como al servidor
  • Vi completa la charla de Dan sobre "2 computers" y también sus publicaciones recientes sobre RSC
    • Dan es el mejor explicando dentro del ecosistema React, pero si una tecnología necesita explicarse de forma tan difícil, entonces
      1. realmente es una tecnología innecesaria, o
      2. hay un problema con la abstracción
    • La mayoría de los desarrolladores frontend todavía no entienden por completo el concepto de RSC
    • Vercel convirtió a Next.js en el framework React por defecto, y la adopción de RSC también se expandió con ese impulso
    • Incluso quienes usan Next.js muchas veces no entienden claramente los límites de los Server Components, y hay bastante adopción tipo “cargo cult”
    • También resulta sospechoso que React no haya aceptado PRs relacionados con Vite. Da la impresión de que el impulso a RSC quizá no sea realmente para usuarios o desarrolladores, sino una estrategia de venta de hosting por parte de proveedores de plataforma
    • Viéndolo en retrospectiva, también parece que Vercel contrató en masa al equipo original de React con la intención de liderar su futuro
    • También se señala que ese juicio sobre las motivaciones o el contexto histórico es incorrecto, y se explica el estado actual del soporte para Vite
    • Se menciona que la integración con Vite está siendo mejorada actualmente por el equipo de Vite debido a la limitación técnica de que en entorno DEV se necesita bundling
    • La idea de que la gente no entiende RSC se considera un argumento lógicamente circular
    • A uno puede no gustarle RSC, pero aun así hay suficientes ideas interesantes dentro que pueden adaptarse a otras tecnologías
    • Más que convencer a todos, la idea sería que cada quien tome las partes curiosas y útiles
  • Claro, todavía puedes construir una SPA como sitio estático y subirla a un CDN, y Next.js también puede self-hostearse en modo “dynamic”
    • Aun así, sigue siendo difícil implementar por completo fuera de Vercel toda la funcionalidad de serverless rendering de Next.js (por cierta “magia” no documentada)
    • Para este problema también se propuso oficialmente introducir adaptadores para ofrecer una API consistente en múltiples plataformas: https://github.com/vercel/next.js/discussions/77740
    • Mi postura es que el impulso a RSC no viene tanto por ganancias corporativas, sino del reconocimiento de que el patrón clásico de construir sitios web (SSR + un poco de progressive enhancement en cliente) realmente tiene muchas ventajas
    • Incluso con SSR existe el problema de que demasiada lógica de negocio termina moviéndose innecesariamente al cliente
  • RSC en sí es una tecnología interesante, pero en producción no me parece tan razonable
    • Existe la carga de mantener a gran escala servidores backend Node/Bun para renderizar componentes complejos
    • Preferiría mil veces una combinación de páginas estáticas o una React SPA + servidor API en Go
    • Se puede lograr un resultado parecido con muchos menos recursos
  • Que una tecnología nueva sea difícil de explicar no significa necesariamente que sea innecesaria o una mala abstracción; hay problemas cuya complejidad sí vale la pena asumir
    • Yo prefiero observar cómo evoluciona esta tecnología en el futuro
  • También pienso que la estructura de código de RSC podría usarse para construir páginas estáticas dividiendo HTML/CSS/JS en piezas pequeñas
    • Si reemplazas el placeholder $1 del artículo por un URI, quizá ni siquiera necesites un servidor (la mayoría de los casos no requieren SSR dinámico obligatoriamente)
    • La desventaja es que, cuando cambia el contenido, es importante asegurar velocidad en el pipeline de actualización, especialmente para despliegues en streaming de sitios estáticos compilados a S3
    • Por ejemplo, en un sitio de noticias con muchos artículos prerenderizados, si cambia solo parte del contenido, hace falta un manejo inteligente de diff de contenido para reconstruir eficientemente solo esa parte
  • En la práctica, muchas veces se habla de optimización de rendimiento mientras desde el frontend se cargan varios MB de datos y se procesa lógica compleja por milisegundos, cuando en realidad una mejora de BFF o de arquitectura, o una API más lean, sería una solución mucho más productiva
    • Hubo intentos con GraphQL, http2, etc., pero al final no resolvieron el problema de fondo, y sin evolución de los estándares web no habrá un cambio de paradigma
    • Los frameworks nuevos también tienen esa misma limitación
  • Se explica que RSC, como se menciona al final del artículo, esencialmente cumple el rol de un BFF (Backend for Frontend)
  • Hay quien opina que depende de qué se quiera decir con “reducir ms de carga de página”
    • Si quieres optimizar el Time to first render o el time to visually complete, entonces mandar primero una skeleton UI vacía y luego hidratar con datos de la API suele sentirse como lo más rápido
    • En cambio, si quieres mejorar el time to first input o el time to interactive, necesitas poder renderizar de inmediato los datos del usuario, y en ese caso el backend tiene mucha ventaja porque minimiza llamadas de red
    • En la mayoría de los casos, los usuarios prefieren más esto último; para apps SaaS tipo CRUD conviene el renderizado del lado del servidor, mientras que para apps donde el diseño es clave, como Figma, encaja mejor un cliente con datos estáticos más fetches adicionales
    • No existe una “única solución para todos los problemas”; el punto de optimización es una elección subjetiva
    • También hay muchos factores que influyen en la elección técnica, como la experiencia de desarrollo o la estructura del equipo
  • Gracias a esto ahora entiendo por qué, cuando carga Facebook, el contenido principal siempre termina renderizándose al final
  • Aparece una pregunta sobre qué significa BFF aquí
  • También hay quien dice que hay demasiadas siglas y pregunta qué significan FE y BFF
  • Yo no querría usar directamente la idea de Progressive JSON, y creo que hay varias alternativas
    • La solución más simple sería dividir un único objeto JSON enorme en varios, o sea, enviarlo como ‘JSON lines’
    • La información de encabezado se envía una sola vez, y los arreglos grandes se mandan línea por línea para hacer más eficiente el procesamiento en stream
    • Si el objeto es aún más grande, este enfoque puede aplicarse recursivamente, aunque podría volverse demasiado complejo
    • También se puede separar el parsing progresivo garantizando explícitamente desde el servidor el orden de las propiedades
    • Al final, quizá no sirva para estructuras realmente gigantes, pero sí es una herramienta bastante práctica para los casos más comunes de JSON grande
  • Sin marcar explícitamente huecos, también se puede enviar mensajes de streaming en secuencia y mandar solo los deltas (diff)
    • Usando un formato de delta llamado ‘Mendoza’, se pueden transmitir patches (diffs) de forma muy compacta en Go y JS/Typescript: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • Como con el diff binario de zstd o con Mendoza, se pueden aplicar parches eficientemente guardando en memoria solo parte de los datos serializados
    • React también necesita un enfoque para comparar diferencias o inyectar solo cambios, así que es una aproximación con sentido
  • En el streaming de datos de UI, no basta con arreglos vacíos o null; hace falta información adicional sobre qué datos siguen pendientes
    • El payload de streaming de GraphQL elige un enfoque mixto entre un esquema de datos válido, información sobre lo aún no recibido y el manejo posterior de patches
  • Saber qué parte es un “hueco” facilita mostrar estados de carga
  • Para decodificar JSON de forma progresiva en el cliente, se presenta la librería jsonriver: https://github.com/rictic/jsonriver
    • Tiene una API muy simple, buen rendimiento y bastantes pruebas
    • Va parseando fragmentos de texto en streaming y los convierte gradualmente en valores cada vez más completos
    • Garantiza que el resultado final es idéntico a JSON.parse
  • Si se trata de datos en árbol, parece un enfoque interesante que podría aplicarse a cualquier estructura
    • Los datos en árbol pueden expresarse con vectores de parent, type y data, más una string table, y así todo lo demás se reduce a unos pocos enteros
    • La string table y la información de tipos pueden enviarse de entrada como encabezado, y luego transmitir por streaming los chunks de parent y data por nodo
    • Para streaming en depth-first o breadth-first basta con cambiar el orden de los chunks
    • Parece que podría mejorar mucho la UX de tiempos de carga en apps sobre red
    • Alternando el envío de tablas y chunks de nodos, se puede visualizar el árbol en la web en cualquier orden
    • Con preorder traversal y la información de profundidad, incluso se puede reconstruir la estructura del árbol sin node id
    • Hacer una pequeña librería con esta idea también parece algo valioso
  • Se argumenta que la mayoría de las apps no necesitan un sistema de carga tan “sofisticado”, y que en la mayoría de los casos se puede reemplazar simplemente con varias llamadas API
    • La respuesta es que lo único que se intentaba era explicar cómo funciona el wire protocol de RSC, no recomendar que la gente implemente esto por su cuenta
    • Entender los principios entre distintas herramientas ayuda al final a tomar ideas o remezclarlas en diferentes contextos
    • Hay quien cree que la estrategia de múltiples llamadas tiene el problema n*n+1, pero en vez de transmitir los objetos anidados al estilo OOP/ORM, se pueden mandar de forma plana, como en el caso de los comentarios
    • En ese caso, endpoints con tipos bien definidos, como con protobuf, también tendrían ventajas
    • Si separas los comments, bastan 2 llamadas (página + post, y comentarios aparte), y eso además permite optimizaciones de pre-render
    • Si ya existen buenas herramientas predefinidas, no necesariamente vale la pena hacer una personalización profunda elevando demasiado la complejidad real de implementación
    • Hay que reconocer que una funcionalidad excesivamente compleja puede terminar perjudicando a usuarios o desarrolladores
    • Como aquello de que 640K era suficiente para todos, también se piensa que progressive/partial reads podrían jugar un papel realmente importante en la velocidad de UX en la era WASM
    • Si a una codificación binaria como protobuf se le suman partial reads y un streaming bien definido, la carga para ingeniería aumenta, pero el resultado en UX podría mejorar mucho
  • Se opina que Progressive JPEG es necesario por la naturaleza de los archivos multimedia, pero que en Text/HTML no haría falta, y que la situación actual es paradójica porque solo añade complejidad como resultado de bundles de JS demasiado grandes
    • Se señala que la causa real de la lentitud no siempre es simplemente el “tamaño” de los datos
    • Si la consulta de datos en el servidor tarda mucho, o la red es lenta, el progressive reveal también tiene sentido
    • En vez de esperar a que todos los datos estén completos, el renderizado por etapas mostrando una loading UI en momentos apropiados puede beneficiar de verdad la experiencia del usuario
  • También se piensa que la estrategia de separar endpoints ya ofrece varias ventajas: evitar Head of line blocking, mejorar opciones de filtrado, permitir actualizaciones en vivo y optimizaciones de rendimiento independientes, etc.
    • Desde esta perspectiva, el problema de fondo sería intentar tratar la aplicación como una plataforma de documentos (document platform)
    • Las aplicaciones reales no funcionan como “documentos”, y cerrar esa brecha termina exigiendo mucho código e infraestructura adicional
    • Para complementar, se enlazan dos textos largos sobre las desventajas reales de adoptar endpoints separados y hacia dónde podría evolucionar esto: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • En resumen, los endpoints terminan convirtiéndose en un contrato de API “oficial” entre servidor y cliente, y a medida que el código se modulariza, eso puede perjudicar el rendimiento (por fenómenos como waterfalls)
    • Tomar las decisiones de una vez en el servidor (coalescing) puede ser una alternativa mejor tanto en rendimiento como en flexibilidad estructural