Renderização na Web

Onde devemos implementar a lógica e a renderização nos nossos aplicativos? Devemos usar a renderização do lado do servidor? E a reidratação? Vamos encontrar algumas respostas.

Addy Osmani
Addy Osmani

Como desenvolvedores, muitas vezes nos deparamos com decisões que afetam toda a arquitetura dos nossos aplicativos. Uma das principais decisões que os desenvolvedores da Web precisam tomar é onde implementar a lógica e a renderização nos aplicativos. Isso pode ser difícil, já que há várias maneiras diferentes de criar um site.

Nossa compreensão desse espaço é fundamentada por nosso trabalho no Chrome ao conversar com sites grandes nos últimos anos. De modo geral, incentivamos os desenvolvedores a considerar a renderização no lado do servidor ou a renderização estática em vez de uma abordagem de reidratação completa.

Para entender melhor as arquiteturas que escolhemos ao tomar essa decisão, precisamos ter uma compreensão sólida de cada abordagem e da terminologia consistente a ser usada ao falar sobre elas. As diferenças entre essas abordagens ajudam a ilustrar as vantagens e desvantagens da renderização na Web pela perspectiva do desempenho.

Terminologia

Renderização

  • Renderização do lado do servidor (SSR, na sigla em inglês): renderiza um app universal ou do lado do cliente para HTML no servidor.
  • Renderização do lado do cliente (CSR):renderiza um app em um navegador via JavaScript para modificar o DOM.
  • Reidratação: "inicializar" visualizações JavaScript no cliente para que reutilizem a árvore e os dados DOM do HTML renderizado pelo servidor.
  • Pré-renderização:execução de um aplicativo do lado do cliente durante a compilação para capturar o estado inicial dele como HTML estático.

Desempenho

Renderização do lado do servidor

A renderização pelo servidor gera o HTML completo de uma página no servidor em resposta à navegação. Isso evita idas e voltas adicionais para busca de dados e modelos no cliente, já que isso é processado antes de o navegador receber uma resposta.

A renderização do lado do servidor geralmente produz uma FCP rápida. A execução da lógica da página e da renderização no servidor evita o envio de muitos JavaScripts ao cliente. Isso ajuda a reduzir o TBT de uma página, o que também pode levar a um INP mais baixo, já que a conversa principal não é bloqueada com tanta frequência durante o carregamento da página. Quando a linha de execução principal é bloqueada com menos frequência, as interações do usuário têm mais oportunidades de serem executadas mais rapidamente. Isso faz sentido, já que, com a renderização do lado do servidor, você está apenas enviando texto e links para o navegador do usuário. Essa abordagem pode funcionar bem em um grande espectro de condições de dispositivo e rede, além de abrir otimizações interessantes do navegador, como a análise de streaming de documentos.

Diagrama mostrando a renderização do lado do servidor e a execução do JS afetando FCP e TTI.

Com a renderização pelo servidor, é menos provável que os usuários fiquem aguardando a execução do JavaScript vinculado à CPU antes de usar seu site. Mesmo quando o JS de terceiros não pode ser evitado, usar a renderização do lado do servidor para reduzir seus próprios custos com JavaScript pode aumentar o orçamento para o restante. No entanto, há uma desvantagem em potencial com essa abordagem: gerar páginas no servidor leva tempo, o que pode resultar em um TTFB maior.

A renderização pelo servidor é suficiente para seu aplicativo depende em grande parte do tipo de experiência que você está criando. Há um debate antigo sobre os aplicativos corretos da renderização do lado do servidor em relação à renderização do lado do cliente, mas é importante lembrar que você pode optar por usar a renderização do lado do servidor em algumas páginas e não em outras. Alguns sites adotaram técnicas de renderização híbrida e tiveram sucesso. A Netflix renderiza páginas de destino relativamente estáticas, enquanto faz a pré-busca do JS para páginas com interações intensas, oferecendo a essas páginas mais pesadas renderizadas pelo cliente uma chance melhor de carregar rapidamente.

Muitos frameworks, bibliotecas e arquiteturas modernas permitem renderizar o mesmo aplicativo no cliente e no servidor. Essas técnicas podem ser usadas para renderização do lado do servidor. No entanto, as arquiteturas em que a renderização acontece no servidor e no cliente são as próprias classes de solução, com características de desempenho e compensações muito diferentes. Os usuários do React podem usar APIs do DOM do servidor ou soluções criadas com base neles, como Next.js, para renderização no servidor. Os usuários do Vue podem consultar o guia de renderização do lado do servidor ou o Nuxt da Vue. O Angular tem Universal. No entanto, as soluções mais populares empregam alguma forma de hidratação. Portanto, esteja ciente da abordagem em uso antes de selecionar uma ferramenta.

Renderização estática

A renderização estática ocorre no tempo de build. Essa abordagem oferece uma FCP rápida e também um TBT e um INP mais baixos, supondo que a quantidade de JS do lado do cliente seja limitada. Ao contrário da renderização pelo servidor, ela também consegue alcançar um TTFB rápido e consistente, já que o HTML para uma página não precisa ser gerado dinamicamente no servidor. Geralmente, a renderização estática significa produzir um arquivo HTML separado para cada URL com antecedência. Com respostas HTML geradas com antecedência, as renderizações estáticas podem ser implantadas em várias CDNs para aproveitar o armazenamento em cache da borda.

Diagrama mostrando a renderização estática e a execução opcional de JS afetando FCP e TTI.

Existem soluções para renderização estática de todos os tamanhos e formas. Ferramentas como o Gatsby são projetadas para que os desenvolvedores sintam que os aplicativos deles estão sendo renderizados de maneira dinâmica, em vez de gerados como uma etapa de build. As ferramentas de geração de sites estáticos, como 11ty, Jekyll e Metalsmith, adotam a natureza estática deles, proporcionando uma abordagem mais baseada em modelos.

Uma das desvantagens da renderização estática é que os arquivos HTML individuais precisam ser gerados para cada URL possível. Isso pode ser desafiador ou até inviável quando não é possível prever quais serão os URLs com antecedência ou para sites com um grande número de páginas exclusivas.

Os usuários do React podem já conhecer o Gatsby, a exportação estática do Next.js ou o Navi. Todos esses recursos facilitam a criação de páginas usando componentes. No entanto, é importante entender a diferença entre renderização estática e pré-renderização: as páginas renderizadas estáticas são interativas sem a necessidade de executar muito JavaScript do lado do cliente, enquanto a pré-renderização melhora a FCP de um aplicativo de página única que precisa ser inicializado no cliente para que as páginas sejam realmente interativas.

Se você não tiver certeza se uma determinada solução é renderização estática ou pré-renderização, tente desativar o JavaScript e carregar a página que você quer testar. Em páginas renderizadas estaticamente, a maioria das funcionalidades continuará existindo sem o JavaScript ativado. Para páginas pré-renderizadas, ainda pode haver algumas funcionalidades básicas, como links, mas a maior parte da página ficará inerte.

Outro teste útil é usar a limitação de rede no Chrome DevTools e observar quanto JavaScript foi transferido por download antes que uma página se torne interativa. A pré-renderização geralmente requer mais JavaScript para se tornar interativo, e esse JavaScript tende a ser mais complexo do que a abordagem de aprimoramento progressivo usada pela renderização estática.

Renderização do lado do servidor versus renderização estática

A renderização no servidor não é uma solução perfeita, já que a natureza dinâmica dela pode ter custos significativos de sobrecarga de computação. Muitas soluções de renderização no servidor não são limpas antecipadamente, podem atrasar o TTFB ou dobrar os dados que estão sendo enviados (por exemplo, o estado in-line usado pelo JavaScript no cliente). No React, a renderToString() pode ser lenta, já que é síncrona e com uma única linha de execução. As APIs mais recentes do DOM do servidor React oferecem suporte a streaming, que pode receber a parte inicial de uma resposta HTML ao navegador mais cedo, enquanto o restante ainda está sendo gerado no servidor.

Conseguir a renderização correta no lado do servidor pode envolver a localização ou criação de uma solução para o armazenamento em cache de componentes, o gerenciamento do consumo de memória, a aplicação de técnicas de memoização e outras questões. Geralmente, o mesmo aplicativo está sendo processado/recriado várias vezes: uma vez no cliente e outra no servidor. O fato de a renderização do lado do servidor fazer algo aparecer mais cedo não significa que você terá menos trabalho a fazer. Se você tiver muito trabalho no cliente depois que uma resposta HTML gerada pelo servidor chegar ao cliente, isso ainda poderá gerar mais TBT e INP para seu site.

A renderização do lado do servidor produz HTML sob demanda para cada URL, mas pode ser mais lenta do que apenas a veiculação de conteúdo renderizado estático. Se for possível colocar o trabalho extra, a renderização no lado do servidor e o armazenamento em cache HTML poderão reduzir significativamente o tempo de renderização do servidor. A vantagem para a renderização pelo servidor é a capacidade de extrair mais dados "ativos" e responder a um conjunto mais completo de solicitações do que é possível com a renderização estática. As páginas que exigem personalização são um exemplo concreto do tipo de solicitação que não funcionaria bem com a renderização estática.

A renderização no lado do servidor também pode apresentar decisões interessantes ao criar um PWA: é melhor usar o armazenamento em cache do service worker de página inteira ou apenas renderizar partes individuais do conteúdo?

Renderização do lado do cliente

Renderização do lado do cliente significa renderizar páginas diretamente no navegador com JavaScript. Toda a lógica, busca de dados, modelos e roteamento são tratados no cliente, e não no servidor. O resultado efetivo é que mais dados são transmitidos do servidor para o dispositivo do usuário e isso vem com um conjunto próprio de compensações.

A renderização no lado do cliente pode ser difícil de obter e manter rapidamente em dispositivos móveis. Se for feito um trabalho mínimo, a renderização do lado do cliente poderá se aproximar do desempenho da renderização pura do lado do servidor, mantendo um orçamento limitado de JavaScript e entregando valor no menor número possível de ciclos de ida e volta. Scripts e dados críticos podem ser entregues mais rapidamente usando <link rel=preload>, o que faz com que o analisador funcione para você em menos tempo. Também vale a pena avaliar padrões como o PRPL para garantir que as navegações inicial e subsequente sejam instantâneas.

Diagrama mostrando a renderização do lado do cliente afetando FCP e TTI.

A principal desvantagem da renderização do cliente é que a quantidade de JavaScript necessária tende a aumentar à medida que um aplicativo cresce, o que pode ter efeitos negativos no INP de uma página. Isso se torna especialmente difícil com a adição de novas bibliotecas JavaScript, polyfills e códigos de terceiros, que competem pelo poder de processamento e geralmente precisam ser processados antes que o conteúdo de uma página possa ser renderizado.

As experiências que usam a renderização do lado do cliente que dependem de grandes pacotes JavaScript precisam considerar a divisão de código agressiva para diminuir o TBT e o INP durante o carregamento da página, além de carregar o JavaScript lentamente, "veiculando apenas o que você precisa, quando necessário". Para experiências com pouca ou nenhuma interatividade, a renderização pelo servidor pode representar uma solução mais escalonável para esses problemas.

Para quem cria aplicativos de página única, identificar as partes principais da interface do usuário compartilhadas pela maioria das páginas significa que você pode aplicar a técnica de armazenamento em cache do shell do aplicativo. Combinado com os service workers, isso pode melhorar muito a percepção do desempenho em visitas repetidas, já que o HTML do shell do aplicativo e as dependências dele podem ser carregados a partir do CacheStorage muito rapidamente.

Como combinar a renderização do lado do servidor e do lado do cliente via reidratação

Essa abordagem tenta suavizar as compensações entre a renderização do lado do cliente e a do servidor ao fazer as duas coisas. Solicitações de navegação, como carregamentos de página completa ou atualizações, são processadas por um servidor que renderiza o aplicativo em HTML. Em seguida, o JavaScript e os dados usados para renderização são incorporados ao documento resultante. Quando feito com cuidado, ele atinge uma FCP rápida, assim como a renderização do lado do servidor, e depois é renderizada novamente no cliente usando uma técnica chamada (re)hidratação. Essa é uma solução eficaz, mas pode vir com desvantagens consideráveis de desempenho.

A principal desvantagem da renderização pelo servidor com reidratação é que ela pode ter um impacto negativo significativo no TBT e no INP, mesmo que melhore a FCP. As páginas renderizadas do servidor podem parecer enganosamente carregadas e interativas, mas não podem responder à entrada até que os scripts dos componentes do lado do cliente sejam executados e que os manipuladores de eventos tenham sido anexados. Isso pode levar alguns segundos ou até minutos no dispositivo móvel.

Talvez você já tenha passado por essa mesma situação após um tempo que parece que uma página foi carregada, ou que um clique ou um toque não tem efeito. Isso rapidamente se torna frustrante, já que o usuário fica se perguntando por que nada está acontecendo quando tenta interagir com a página.

Um problema de reidratação: um app pelo preço de dois

Problemas de reidratação costumam ser piores do que o atraso de interatividade por causa do JavaScript. Para que o JavaScript do lado do cliente possa “continuar” com precisão de onde o servidor parou sem precisar solicitar novamente todos os dados que o servidor usou para renderizar o HTML, as soluções atuais de renderização do lado do servidor geralmente serializam a resposta das dependências de dados de uma IU no documento como tags de script. O documento HTML resultante contém um alto nível de duplicação:

Documento HTML
contendo a IU serializada, dados inline e um script bundle.js

Como você pode notar, o servidor retorna uma descrição da IU do aplicativo em resposta a uma solicitação de navegação, mas também retorna os dados de origem usados para compor essa IU e uma cópia completa da implementação da IU, que é inicializada no cliente. Essa IU só se tornará interativa após o carregamento e a execução do bundle.js.

Métricas de desempenho coletadas de sites reais usando renderização e reidratação do lado do servidor indicam que seu uso não deve ser recomendado. Em última análise, a razão se resume à experiência do usuário: é extremamente fácil deixar os usuários em um "vale misterioso", onde a interatividade parece ausente, mesmo que a página pareça estar pronta.

Diagrama mostrando a renderização do cliente afetando negativamente o TTI.

No entanto, há esperança para a renderização do lado do servidor com reidratação. A curto prazo, usar somente a renderização do lado do servidor para conteúdo altamente armazenável em cache pode reduzir o TTFB, produzindo resultados semelhantes à pré-renderização. A reidratação de forma incremental, progressiva ou parcial pode ser a chave para tornar essa técnica mais viável no futuro.

Streaming de renderização no lado do servidor e reidratação progressiva

A renderização do lado do servidor teve várias evoluções nos últimos anos.

A renderização de streaming do lado do servidor permite enviar HTML em partes que o navegador pode renderizar progressivamente à medida que é recebido. Isso pode resultar em uma FCP mais rápida, já que a marcação chega aos usuários mais rapidamente. No React, os fluxos assíncronos na [renderToPipeableStream()], em comparação com o renderToString() síncrono, significa que a pressão de retorno é bem tratada.

Também vale a pena considerar a reidratação progressiva, e algo que o React lançou. Com essa abordagem, as partes individuais de um aplicativo renderizado pelo servidor são "inicializadas" ao longo do tempo, em vez da abordagem comum atual de inicializar todo o aplicativo de uma só vez. Isso pode ajudar a reduzir a quantidade de JavaScript necessária para tornar as páginas interativas, já que a atualização do lado do cliente de partes de baixa prioridade da página pode ser adiada para evitar o bloqueio da thread principal, permitindo que as interações do usuário ocorram mais rapidamente após o usuário iniciá-las.

A reidratação progressiva também pode ajudar a evitar uma das armadilhas mais comuns de reidratação de renderização no lado do servidor, em que uma árvore DOM renderizada pelo servidor é destruída e recriada imediatamente. Na maioria das vezes, porque a renderização síncrona inicial do lado do cliente exigia dados que não estavam prontos, talvez aguardando a resolução de uma Promise.

Reidratação parcial

A reidratação parcial tem sido difícil de implementar. Essa abordagem é uma extensão da ideia de reidratação progressiva, em que as peças individuais (componentes/visualizações/árvores) a serem reidratadas progressivamente são analisadas, e aquelas com pouca interatividade ou nenhuma reatividade são identificadas. Para cada uma dessas partes quase estáticas, o código JavaScript correspondente é transformado em referências inertes e funcionalidade decorativa, reduzindo seu consumo no lado do cliente para quase zero.

A abordagem de hidratação parcial tem os próprios problemas e concessões. Isso apresenta alguns desafios interessantes para o armazenamento em cache, e a navegação do lado do cliente significa que não podemos presumir que o HTML renderizado pelo servidor para partes inertes do aplicativo esteja disponível sem um carregamento de página completo.

Renderização trisomórfica

Se os service workers forem uma opção para você, a renderização "trisomórfica" também poderá ser interessante. É uma técnica em que você pode usar o streaming de renderização do lado do servidor para navegações iniciais/não JS e, em seguida, fazer com que o service worker assuma a renderização de HTML para navegações depois de instalado. Isso mantém os componentes e modelos armazenados em cache atualizados e permite navegações no estilo SPA para renderizar novas visualizações na mesma sessão. Essa abordagem funciona melhor quando é possível compartilhar os mesmos modelos e códigos de roteamento entre o servidor, a página do cliente e o service worker.

Diagrama de renderização trisomórfica mostrando um navegador e um service worker se comunicando com o servidor.

Considerações sobre SEO

As equipes geralmente consideram o impacto da SEO ao escolher uma estratégia de renderização na Web. A renderização pelo servidor geralmente é escolhida para proporcionar uma experiência com aparência completa que os rastreadores podem interpretar com facilidade. Os rastreadores podem entender o JavaScript, mas geralmente há limitações que vale a pena conhecer na renderização. A renderização do lado do cliente pode funcionar, mas geralmente não sem testes adicionais e trabalho de etapas. Mais recentemente, a renderização dinâmica também se tornou uma opção a considerar caso sua arquitetura dependa muito de JavaScript no lado do cliente.

Em caso de dúvida, a ferramenta de teste de compatibilidade com dispositivos móveis é inestimável para verificar se a abordagem escolhida faz o que você esperava. Ele mostra uma visualização de como qualquer página aparece para o rastreador do Google, o conteúdo HTML serializado encontrado (após a execução do JavaScript) e todos os erros encontrados durante a renderização.

Captura de tela da IU de teste de compatibilidade com dispositivos móveis.

Conclusão

Ao decidir sobre uma abordagem de renderização, avalie e entenda quais são os gargalos. Considere se a renderização estática ou a renderização pelo servidor podem ser a maior parte desse processo. Não há problema em enviar HTML com o mínimo de JavaScript para obter uma experiência interativa. Confira um infográfico útil que mostra o espectro servidor-cliente:

Infográfico mostrando o espectro das opções descritas neste artigo.

Créditos

Agradecemos a todos pelas avaliações e inspiração:

Jeffrey Posnick, Houssein Djirdeh, Shubhie Panicker, Chris Harrelson e Sebastian Markbåge