15 pontos por GN⁺ 2025-06-02 | 1 comentários | Compartilhar no WhatsApp
  • Assim como no JPEG progressivo, os dados em JSON também podem ser enviados primeiro de forma incompleta, permitindo que o cliente aproveite gradualmente todo o conteúdo
  • O método tradicional de parsing de JSON tem a ineficiência de não permitir nenhuma ação até que todos os dados sejam recebidos por completo
  • Com uma abordagem breadth-first, os dados são divididos em vários chunks (partes), e as seções ainda não prontas são representadas como Promises e preenchidas progressivamente conforme ficam disponíveis, permitindo que o cliente use até mesmo dados incompletos
  • Esse conceito é a principal inovação dos React Server Components (RSC), e o <Suspense> controla estados de carregamento intencionais em etapas
  • Ao separar o streaming de dados do fluxo intencional de carregamento da UI, é possível oferecer uma experiência do usuário mais flexível

A ideia de JPEG progressivo e JSON progressivo

  • Um JPEG progressivo, em vez de carregar a imagem de uma vez só de cima para baixo, primeiro mostra a imagem inteira de forma borrada e depois vai ficando mais nítida aos poucos
  • De forma semelhante, ao aplicar uma abordagem progressiva à transmissão de JSON, é possível usar parte dos dados imediatamente, sem esperar que tudo esteja completo
  • Em uma estrutura de dados JSON de exemplo, o método comum só permite o parsing depois que o último byte tiver sido recebido
  • Por causa disso, o cliente precisa esperar até que tudo seja transmitido, inclusive as partes lentas do servidor (por exemplo, carregar comments de um banco de dados lento), o que é um padrão atual bastante ineficiente

Limites de um parser de JSON por streaming

  • Ao introduzir um parser de JSON por streaming, é possível criar uma árvore de objetos intermediária e incompleta
  • Porém, quando apenas parte dos campos de cada objeto é transmitida (por exemplo, footer, ou uma lista com vários comment), surgem problemas de incompatibilidade de tipos e dificuldade para identificar o grau de completude, reduzindo sua utilidade
  • Assim como no render streaming de HTML, ao processar o stream em ordem, uma parte lenta acaba atrasando o resultado inteiro
  • Esse é um dos motivos pelos quais o uso de JSON por streaming não é comum

Proposta de estrutura para Progressive JSON

  • Em vez do streaming tradicional em profundidade, isto é, percorrendo e transmitindo até os níveis inferiores da árvore, adota-se uma abordagem breadth-first (em largura)
  • Primeiro, apenas o objeto de nível superior é enviado, enquanto os valores abaixo ficam como placeholders semelhantes a Promises, sendo preenchidos em chunks separados conforme ficam prontos
  • Por exemplo, sempre que o servidor termina o carregamento assíncrono de dados, ele envia o chunk correspondente, e o cliente pode usar apenas o que já estiver disponível
  • Isso permite recebimento assíncrono de dados (carregamento antecipado) e evita a necessidade de esperar pelo término de várias partes lentas antes de aproveitar qualquer resultado
  • Se o cliente for projetado para lidar bem com recebimento não sequencial por chunk e parcialmente sequencial, o servidor pode aplicar com flexibilidade várias estratégias de divisão em chunks

Inlining e Outlining: transmissão de dados eficiente

  • Um formato de streaming de JSON progressivo pode, sem duplicar armazenamento, extrair em um chunk separado objetos reutilizados (por exemplo, o mesmo userInfo referenciado em vários lugares) e permitir a mesma referência em cada posição
  • Apenas as partes lentas são separadas e enviadas como placeholders, enquanto o restante é preenchido imediatamente, criando um stream de dados mais eficiente
  • Quando o mesmo objeto aparece várias vezes, é possível enviá-lo uma única vez e reutilizá-lo (Outlining)
  • Dessa forma, até referências circulares (estruturas em que um objeto referencia a si mesmo) podem ser serializadas naturalmente por meio de referências indiretas entre chunks, sem a dificuldade típica do JSON comum

A implementação de streaming progressivo nos React Server Components (RSC)

  • Os React Server Components reais são um exemplo representativo de aplicação do modelo de streaming progressivo de JSON
    • O servidor usa uma estrutura que carrega dados externos (por exemplo, Post e Comments) de forma assíncrona
    • No cliente, as partes que ainda não chegaram ficam como Promises e a UI é renderizada progressivamente na ordem em que ficam prontas
  • O React usa <Suspense> para definir estados de carregamento intencionais
    • Para evitar saltos visuais desnecessários na experiência do usuário, o estado de Promise (os “buracos”) não é mostrado imediatamente; em vez disso, o fallback de <Suspense> permite encenar o carregamento em etapas
    • Mesmo que os dados cheguem rapidamente, o desenvolvedor pode controlar a exposição progressiva da UI de acordo com as etapas planejadas

Resumo e implicações

  • A principal inovação dos React Server Components está em fazer streaming progressivo das props da árvore de componentes de fora para dentro
  • Portanto, não é necessário esperar que o servidor prepare todos os dados por completo; é possível mostrar primeiro as partes principais e controlar com mais precisão os estados de espera de carregamento
  • Não basta apenas o streaming em si; também é necessário suporte estrutural, como um modelo de programação que o aproveite (como o <Suspense> do React)
  • Com isso, é possível aliviar gargalos dos métodos tradicionais de transmissão, como o problema de uma parte lenta dos dados atrasar todo o restante

1 comentários

 
GN⁺ 2025-06-02
Opiniões do Hacker News
  • Parece haver uma tendência de algumas pessoas levarem este texto ao pé da letra demais; a explicação é que Dan Abramov não está propondo um novo formato chamado Progressive JSON
    • O texto explica a ideia de React Server Components e o processo de representar a árvore de componentes como objetos JavaScript e transmiti-la em forma de stream
    • Essa abordagem permite deixar “buracos” na árvore de componentes do React, mostrando inicialmente estados de carregamento ou skeleton UI e renderizando completamente essas partes quando os dados reais chegam do servidor
    • Isso permite indicadores de carregamento mais granulares e uma exibição inicial mais rápida da tela
  • Na minha opinião, tudo bem que as pessoas expandam essa ideia e a apliquem de outras formas
    • A intenção era explicar a forma de serializar dados do RSC não como algo limitado ao React, mas como um padrão mais geral
    • A esperança é que várias ideias encontradas em React Server Components acabem sendo absorvidas por outras tecnologias ou ecossistemas
  • Eu não gosto muito da abordagem de progressive loading, especialmente da experiência de conteúdo ficando se movendo o tempo todo
    • O padrão de mostrar uma UI vazia durante o carregamento me incomoda em especial
  • Eu já tinha visto algo parecido quando usava Ember até pouco tempo atrás, e lembro como era doloroso escrever endpoints Ajax
    • A intenção parecia ser reorganizar a estrutura em árvore para que alguns elementos filhos fossem parar no fim do arquivo, tornando o processamento de DAGs (grafos acíclicos dirigidos) mais eficiente
    • Com um parser de streaming no estilo SAX, dá para começar a pintar a interface antes, à medida que os dados chegam parcialmente
    • Mas, em uma VM single-thread, se a ordem do trabalho for mal planejada, o risco é só aumentar os problemas
  • Eu já uso na prática streaming de JSON parcial (Progressive JSON) em integrações com ferramentas de IA
    • Minha experiência real é que essa abordagem tem valor prático tanto no cliente quanto no servidor, não só para RSC, mas em vários outros contextos
  • Eu acompanhei a apresentação "2 computers" do Dan e também os posts recentes sobre RSC
    • Dan é o melhor explicador do ecossistema React, mas se uma tecnologia precisa ser explicada de forma tão difícil assim, então
      1. ou é uma tecnologia de que realmente não precisamos
      2. ou há algum problema na abstração
    • A grande maioria dos desenvolvedores front-end ainda não entendeu completamente o conceito de RSC
    • A Vercel transformou o Next.js no framework React padrão, e a adoção de RSC se espalhou nessa mesma onda
    • Mesmo quem usa Next.js muitas vezes não entende claramente as fronteiras de Server Components, e há muita adoção em estilo “cargo cult”
    • Também acho suspeito o fato de o React não ter aceitado o PR relacionado ao Vite. Fico pensando se o impulso em torno de RSC não seria, na prática, menos para usuários ou desenvolvedores e mais uma estratégia de venda da plataforma de hospedagem dessas empresas de plataforma
    • Olhando para trás, a contratação em massa do time original do React pela Vercel também parece uma tentativa de liderar o futuro do React
    • Houve uma correção dizendo que essa leitura sobre motivações históricas e contexto estava errada, junto com uma explicação do estado atual do suporte ao Vite
    • Foi mencionado que a integração com o Vite está sendo trabalhada pelo time do Vite por causa da limitação técnica de precisar de bundling no ambiente DEV
    • A ideia de que as pessoas não entendem RSC seria, segundo essa visão, um argumento logicamente circular
    • Dá para não gostar de RSC, mas ainda assim há várias ideias interessantes ali que podem ser reaproveitadas em outras tecnologias
    • Em vez de convencer, a intenção é que cada um aproveite as partes curiosas e úteis
  • Claro, ainda é possível transformar uma SPA em um site estático e colocá-lo em uma CDN, e o Next.js também pode ser self-hosted em modo “dynamic”
    • Mas existe a realidade de que é difícil implementar de forma completa, fora da Vercel, todos os recursos de serverless rendering do Next.js (por causa de “mágicas” não documentadas)
    • Esse problema também motivou uma proposta oficial de adoção de adapters para oferecer APIs consistentes em múltiplas plataformas: https://github.com/vercel/next.js/discussions/77740
    • Minha posição é que o impulso em torno de RSC não vem tanto de interesse corporativo, mas da percepção de que o padrão tradicional de construir sites (SSR + um pouco de progressive enhancement no cliente) realmente tem muitas vantagens
    • Mesmo só com SSR já existe o problema de mover lógica de negócio demais para o cliente sem necessidade
  • O RSC em si é uma tecnologia interessante, mas me parece pouco razoável na prática
    • Existe o custo de manter em larga escala servidores de backend Node/Bun para renderizar componentes complexos
    • Uma combinação de páginas estáticas ou React SPA + servidor de API em Go parece bem mais eficiente
    • Dá para obter resultados parecidos com muito menos recursos
  • O fato de a explicação de uma nova tecnologia ser complexa não significa necessariamente que ela seja desnecessária ou uma abstração ruim; há problemas cuja complexidade vale a pena assumir
    • A visão é de observar como essa tecnologia vai evoluir daqui para frente
  • Acho que também seria possível usar a estrutura de código do RSC para gerar páginas estáticas dividindo HTML/CSS/JS em pequenos pedaços
    • O placeholder ‘$1’ proposto no texto também poderia ser trocado por URIs, e talvez nem fosse necessário um servidor (na maioria dos casos não é obrigatório ter SSR dinâmico)
    • A desvantagem é que, nesse modelo, quando o conteúdo muda, a velocidade do pipeline de atualização se torna importante, especialmente para deploy em streaming de sites estáticos compilados em S3
    • Por exemplo, em um site de jornal com muitas matérias pré-renderizadas, seria necessário um tratamento inteligente de diff de conteúdo para rebuildar eficientemente apenas as partes alteradas
  • Na prática, vejo muita “otimização de performance” trazendo vários MB de dados para o front-end e processando lógica complexa em milissegundos, quando na verdade melhorar o BFF ou a arquitetura, ou construir APIs mais lean, costuma ser uma solução muito mais produtiva
    • Já houve tentativas com GraphQL, http2 etc., mas a opinião é que isso não resolveu o problema de fundo, e que sem evolução dos padrões web não haverá mudança de paradigma
    • Frameworks novos também compartilham essa mesma limitação
  • A explicação é que o RSC, como descrito no fim do texto, essencialmente faz o papel de BFF (Backend for Frontend)
  • Houve a observação de que tudo depende do que se quer dizer com “reduzir ms no carregamento da página”
    • Se a otimização for para time to first render e time to visually complete, mandar primeiro uma skeleton UI vazia e depois hidratar com dados vindos da API parece, na percepção do usuário, o mais rápido
    • Por outro lado, se a meta for melhorar time to first input e time to interactive, é preciso conseguir renderizar os dados do usuário logo de cara, e nesse caso o backend leva ampla vantagem (minimizando chamadas de rede)
    • Na maioria dos casos os usuários preferem esse segundo caminho, e apps CRUD/SaaS se beneficiam mais de renderização no servidor, enquanto apps centrados em design, como o Figma, combinam melhor com dados estáticos no cliente + fetch adicional
    • Não existe “uma única solução para todos os problemas”, e o ponto ótimo de otimização é uma escolha subjetiva
    • Há vários fatores que influenciam a escolha tecnológica, como experiência de desenvolvimento e estrutura do time
  • Isso me ajudou a entender por que, quando o Facebook carrega, o conteúdo principal quase sempre é renderizado por último
  • Surgiu a pergunta sobre o que significa BFF nesse contexto
  • Também houve reação perguntando o que significam tantas siglas, como FE e BFF
  • Eu não gostaria de usar a ideia de Progressive JSON diretamente e acho que existem várias alternativas
    • A solução mais simples é dividir um grande objeto JSON em vários menores, ou seja, transmitir como ‘JSON lines’
    • Informações de cabeçalho vão uma vez, e um grande array pode ser enviado linha a linha para tornar o processamento em stream mais eficiente
    • Se o objeto for ainda maior, isso pode ser aplicado recursivamente, embora possa ficar complexo demais
    • Também é possível separar o progressive parsing garantindo explicitamente no servidor a ordem das propriedades
    • No fim, isso talvez não seja tão útil para estruturas realmente gigantes, mas é uma ferramenta bastante prática nos cenários mais comuns de JSON grande
  • Mesmo sem marcar explicitamente os buracos (holes), é possível enviar mensagens em streaming em sequência e transmitir apenas o delta (diff)
    • Usando o formato de delta ‘Mendoza’, é possível enviar patches (diffs) de forma bem compacta em Go e JS/Typescript: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • Assim como no diff binário do zstd ou no Mendoza, é possível fazer patches eficientes mantendo apenas parte dos dados serializados na memória
    • Isso também é uma abordagem significativa para o React, já que ele precisa comparar diferenças ou injetar apenas as mudanças
  • Em streaming de dados de UI, arrays vazios ou null não bastam; é necessário ter informação separada sobre quais dados ainda estão pendentes
    • O payload de streaming do GraphQL escolheu uma abordagem mista: esquema de dados válido, informação sobre partes ainda não recebidas e posterior aplicação de patches
  • Saber quais partes são os “buracos” facilita mostrar estados de loading
  • Para fazer decode progressivo de JSON no cliente, foi indicada a biblioteca jsonriver: https://github.com/rictic/jsonriver
    • A API é muito simples, o desempenho é bom e há testes suficientes
    • Ela faz parse de fragmentos de string transmitidos em stream em valores progressivamente mais completos
    • Garante que o resultado final seja idêntico ao JSON.parse
  • Se forem dados em árvore, isso parece uma abordagem interessante aplicável a qualquer estrutura
    • Dados em árvore podem ser representados com vetores de parent, type e data, além de uma string table, reduzindo todo o resto a poucos inteiros
    • A string table e as informações de tipo podem ser enviadas antecipadamente como cabeçalho, e os chunks dos vetores parent e data podem ser transmitidos em stream por nó
    • Para streaming depth-first ou breadth-first, basta mudar a ordem dos chunks
    • Parece algo que poderia melhorar bastante a UX de tempo de carregamento em apps sobre rede
    • Alternando o envio de tabelas e chunks de nós, seria possível visualizar a árvore na web em qualquer ordem
    • Com preorder traversal e informação de profundidade, dá até para reconstruir a árvore sem node id
    • Fazer uma pequena biblioteca com essa ideia parece uma tentativa válida
  • A alegação é que a maioria dos apps não precisa desse tipo de sistema de loading “sofisticado”, porque na maior parte dos casos várias chamadas de API simples já resolvem
    • A resposta foi que a intenção era apenas explicar como funciona o wire protocol do RSC, não recomendar que as pessoas implementem isso diretamente
    • Entender os princípios por trás de várias ferramentas ajuda depois a pegar emprestadas ou remixar ideias em diferentes contextos
    • Há quem veja na estratégia de múltiplas chamadas um problema de n*n+1, mas em vez de transmitir tudo de forma aninhada em estilo OOP/ORM, também dá para enviar de forma achatada, como no caso dos comentários
    • Nessa linha, endpoints com tipos claros usando protobuf etc. também têm suas vantagens
    • Separando os comments, duas chamadas bastam (página+post e comentários), e isso também ajuda na otimização de pre-render
    • Se já existirem boas ferramentas prontas, talvez não haja necessidade de deep customization nem de elevar demais a complexidade de implementação das opções
    • A posição é que devemos reconhecer que funcionalidades excessivamente complexas podem acabar sendo ruins tanto para usuários quanto para desenvolvedores
    • Como na velha ideia de que 640K era suficiente para todos, há quem pense que progressive/partial reads podem, sim, ter papel real na velocidade de UX na era do WASM
    • Se abordagens de codificação binária como protobuf ganharem partial read e streaming bem definido, o custo para engenharia aumenta, mas o ganho de UX pode ser grande
  • A visão é que Progressive JPEG faz sentido pela natureza de arquivos de mídia, mas em Text/HTML isso não seria necessário, e a situação acaba sendo contraditória: bundles JS maiores gerando ainda mais complexidade
    • Foi apontado que a causa da lentidão nem sempre é simplesmente o “tamanho” dos dados
    • Às vezes a própria consulta de dados no servidor demora, ou a rede é lenta, e nesses casos a exposição progressiva (progressive reveal) continua fazendo sentido
    • Em vez de esperar os dados completos, renderizar por etapas com loading UI em momentos apropriados pode de fato melhorar a experiência do usuário
  • A estratégia de separar endpoints já oferece várias vantagens, como evitar head-of-line blocking, melhorar opções de filtro, permitir live updates e otimizações de performance independentes
    • A posição é que o problema de fundo está em tentar tratar a aplicação como uma document platform
    • Aplicações reais não funcionam como “documentos”, e para compensar essa diferença acaba sendo necessário muito código e infraestrutura adicional
    • Houve complemento com dois textos longos sobre as desvantagens reais de adotar endpoints separados e sobre possíveis direções de evolução: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • Em resumo, endpoints acabam virando um contrato de API “oficial” entre servidor e cliente, e à medida que o código se modulariza isso pode prejudicar a performance (efeitos de waterfall etc.)
    • Consolidar essas decisões no servidor de uma vez só (coalescing) pode ser uma alternativa melhor tanto em performance quanto em flexibilidade estrutural