Renderização na Web

Uma das principais decisões que os desenvolvedores da Web precisam tomar é onde implementar a lógica e a renderização no aplicativo. Isso pode ser difícil porque há muitas maneiras de criar um site.

Nosso entendimento desse espaço é informado pelo trabalho que fizemos no Chrome conversando com grandes sites nos últimos anos. Em geral, recomendamos que os desenvolvedores considerem a renderização do lado do servidor ou estática em vez de uma abordagem de reidratação completa.

Para entender melhor as arquiteturas que escolhemos ao tomar essa decisão, precisamos de uma terminologia consistente e uma estrutura compartilhada para cada abordagem. Assim, você pode avaliar melhor as compensações de cada abordagem de renderização na perspectiva da performance da página.

Terminologia

Primeiro, definimos alguns termos que vamos usar.

Renderização

Renderização do lado do servidor (SSR)
Renderizar um app no servidor para enviar HTML, em vez de JavaScript, ao cliente.
Renderização do lado do cliente (CSR)
Renderização de um app em um navegador usando JavaScript para modificar o DOM.
Pré-renderização
Executar um aplicativo do lado do cliente no momento da build para capturar o estado inicial dele como HTML estático.
Hidratação
Execução de scripts do lado do cliente para adicionar estado e interatividade do aplicativo ao HTML renderizado pelo servidor. A hidratação pressupõe que o DOM não muda.
Reidratação
Embora seja usado com frequência para significar a mesma coisa que hidratação, a reidratação implica atualizar regularmente o DOM com o estado mais recente, inclusive após a hidratação inicial.

Desempenho

Tempo de início até o primeiro byte (TTFB)
O tempo entre o clique em um link e o carregamento do primeiro byte de conteúdo na nova página.
First Contentful Paint (FCP)
O momento em que o conteúdo solicitado (corpo do artigo etc.) fica visível.
Interaction to Next Paint (INP)
Uma métrica representativa que avalia se uma página responde de forma consistente e rápida às entradas do usuário.
Tempo total de bloqueio (TBT)
Uma métrica de proxy para INP que calcula por quanto tempo a linha de execução principal ficou bloqueada durante o carregamento da página.

Renderização do lado do servidor

A renderização do lado do servidor gera o HTML completo de uma página no servidor em resposta à navegação. Isso evita viagens de ida e volta adicionais para busca de dados e criação de modelos no cliente, porque o renderizador os processa antes que o navegador receba uma resposta.

A renderização do lado do servidor geralmente produz um FCP rápido. Executar a lógica da página e renderizar no servidor evita o envio de muito JavaScript para o cliente. Isso ajuda a reduzir o TTBT de uma página, o que também pode levar a um INP menor, porque a linha de execução 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 antes.

Isso faz sentido porque, com a renderização do lado do servidor, você está apenas enviando texto e links para o navegador do usuário. Essa abordagem funciona bem para várias condições de dispositivo e rede e abre otimizações interessantes do navegador, como a análise de documentos de streaming.

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

Com a renderização do lado do servidor, os usuários têm menos chances de ficar esperando a execução do JavaScript vinculado à CPU antes de usar seu site. Mesmo quando não é possível evitar o JavaScript de terceiros, usar a renderização do lado do servidor para reduzir os próprios custos de JavaScript pode dar mais orçamento para o restante. No entanto, essa abordagem tem uma possível desvantagem: gerar páginas no servidor leva tempo, o que pode aumentar o TTFB da página.

Se a renderização do lado do servidor é suficiente para seu aplicativo, isso depende muito do tipo de experiência que você está criando. Há um debate antigo sobre as aplicações corretas da renderização do lado do servidor e do lado do cliente, mas você sempre pode escolher 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 com sucesso. Por exemplo, a Netflix renderiza no servidor as páginas de destino relativamente estáticas, enquanto prefetching o JavaScript para páginas com muitas interações, a essas páginas mais pesadas renderizadas no cliente uma chance maior de carregar rapidamente.

Com muitos frameworks, bibliotecas e arquiteturas modernos, é possível renderizar o mesmo aplicativo no cliente e no servidor. Você pode usar essas técnicas para renderização do lado do servidor. No entanto, as arquiteturas em que a renderização acontece no servidor e no cliente são uma classe de solução com características de desempenho e compensações muito diferentes. Os usuários do React podem usar APIs DOM do servidor ou soluções criadas com base nelas, como o Next.js, para renderização do lado do servidor. Os usuários do Vue podem usar o guia de renderização do lado do servidor ou o Nuxt (links em inglês). O Angular tem o Universal.

No entanto, as soluções mais usadas usam alguma forma de hidratação. Portanto, fique atento às abordagens usadas pela sua ferramenta.

Renderização estática

A renderização estática acontece durante a criação. Essa abordagem oferece um FCP rápido, além de um TBT e INP mais baixos, desde que você limite a quantidade de JavaScript do lado do cliente nas suas páginas. Ao contrário da renderização do lado do servidor, ela também alcança um TTFB consistentemente rápido, porque o HTML de uma página não precisa ser gerado dinamicamente no servidor. Em geral, a renderização estática significa produzir um arquivo HTML separado para cada URL com antecedência. Com respostas HTML geradas com antecedência, é possível implantar renderizações estáticas em várias CDNs para aproveitar o cache de borda.

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

Existem soluções de renderização estática de todos os tipos e tamanhos. Ferramentas como o Gatsby são projetadas para fazer com que os desenvolvedores sintam que o aplicativo está sendo renderizado dinamicamente, e não gerado como uma etapa de build. Ferramentas de geração de sites estáticos, como 11ty, Jekyll e Metalsmith, adotam a natureza estática, oferecendo uma abordagem mais orientada a modelos.

Uma das desvantagens da renderização estática é que ela precisa gerar arquivos HTML individuais para todos os URLs possíveis. Isso pode ser difícil ou até mesmo inviável quando você precisa prever esses URLs com antecedência e para sites com um grande número de páginas únicas.

Os usuários do React talvez conheçam o Gatsby, a exportação estática do Next.js ou o Navi, que facilitam a criação de páginas com base em componentes. No entanto, a renderização estática e a pré-renderização se comportam de maneira diferente: as páginas renderizadas de forma estática são interativas sem precisar executar muito JavaScript do lado do cliente, enquanto a pré-renderização melhora o FCP de um aplicativo de página única que precisa ser inicializado no cliente para tornar as páginas realmente interativas.

Se você não tiver certeza se uma determinada solução é renderização estática ou pré-renderização, desative o JavaScript e carregue a página que quer testar. Para páginas renderizadas estaticamente, a maioria dos recursos interativos ainda existe sem JavaScript. As páginas pré-renderizadas ainda podem ter alguns recursos básicos, como links com JavaScript desativado, mas a maior parte da página fica inerte.

Outro teste útil é usar o limite de rede no Chrome DevTools e ver quanto JavaScript é baixado antes de uma página se tornar interativa. A pré-renderização geralmente precisa de mais JavaScript para se tornar interativa, e esse JavaScript tende a ser mais complexo do que a abordagem de melhoria progressiva usada na renderização estática.

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

A renderização do lado do servidor não é a melhor solução para tudo, porque a natureza dinâmica dela pode gerar custos significativos de sobrecarga de computação. Muitas soluções de renderização do lado do servidor não liberam cedo, atrasam o TTFB ou dobram os dados enviados (por exemplo, estados em linha usados pelo JavaScript no cliente). No React, renderToString() pode ser lento porque é síncrono e de uma única linha de execução. As APIs DOM do servidor React mais recentes oferecem suporte a streaming, que pode enviar a parte inicial de uma resposta HTML ao navegador mais cedo enquanto o restante ainda está sendo gerado no servidor.

Para fazer a renderização do lado do servidor "certa", é necessário encontrar ou criar uma solução para armazenamento em cache de componentes, gerenciar o consumo de memória, usar técnicas de memoização e outras questões. Muitas vezes, você processa ou recria o mesmo app duas vezes, uma no cliente e outra no servidor. A renderização do lado do servidor que mostra o conteúdo mais cedo não necessariamente reduz o trabalho. Se você tiver muito trabalho no cliente depois que uma resposta HTML gerada pelo servidor chegar ao cliente, isso ainda poderá levar a um TBT e um INP mais altos 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 veicular conteúdo estático renderizado. Se você puder fazer o trabalho extra, a renderização do lado do servidor e o armazenamento em cache de HTML podem reduzir significativamente o tempo de renderização do servidor. A vantagem da renderização do lado do 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 precisam de personalização são um exemplo concreto do tipo de solicitação que não funciona bem com a renderização estática.

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

Renderização do lado do cliente

A renderização do lado do cliente significa renderizar páginas diretamente no navegador com JavaScript. Toda a lógica, busca de dados, criação de modelos e roteamento são processados no cliente, em vez de no servidor. O resultado é que mais dados são transmitidos do servidor para o dispositivo do usuário, o que tem um conjunto próprio de compensações.

A renderização do lado do cliente pode ser difícil de fazer e manter rápida para dispositivos móveis. Com um pouco de trabalho para manter um orçamento de JavaScript apertado e entregar valor no menor número possível de viagens de ida e volta, é possível fazer com que a renderização do lado do cliente quase replique o desempenho da renderização pura do lado do servidor. Para que o analisador funcione mais rápido, entregue scripts e dados críticos usando <link rel=preload> Também recomendamos considerar o uso de padrões como PRPL para garantir que as navegações iniciais e subsequentes sejam instantâneas.

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

A principal desvantagem da renderização do lado do cliente é que a quantidade de JavaScript necessária tende a aumentar à medida que um aplicativo cresce, o que pode afetar o INP de uma página. Isso fica ainda mais difícil com a adição de novas bibliotecas JavaScript, polyfills e código de terceiros, que competem por 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 e dependem de grandes pacotes JavaScript precisam considerar o separação de código agressiva para reduzir o TBT e o INP durante o carregamento da página, além de usar o carregamento lento do JavaScript para veicular apenas o que o usuário precisa, quando necessário. Para experiências com pouca ou nenhuma interatividade, a renderização do lado do 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 permite aplicar a técnica de armazenamento em cache do shell do aplicativo. Combinado com service workers, isso pode melhorar muito o desempenho percebido em visitas repetidas, porque a página pode carregar o HTML do shell do aplicativo e as dependências de CacheStorage muito rapidamente.

A reidratação combina a renderização do lado do servidor e do lado do cliente.

A hidratação é uma abordagem que reduz os conflitos entre a renderização do lado do cliente e do servidor fazendo as duas coisas. As solicitações de navegação, como carregamentos ou recarregamentos de página inteira, 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, isso alcança um FCP rápido, como a renderização do lado do servidor, e depois "retoma" a renderização no cliente.

Essa é uma solução eficaz, mas pode ter desvantagens consideráveis de desempenho.

A principal desvantagem da renderização do lado do servidor com reidratação é que ela pode ter um impacto negativo significativo no TBT e no INP, mesmo que melhore o FCP. As páginas renderizadas do lado do servidor podem parecer carregadas e interativas, mas não podem responder à entrada até que os scripts do lado do cliente para componentes sejam executados e os manipuladores de eventos sejam anexados. Em dispositivos móveis, isso pode levar minutos, confundindo e frustrando o usuário.

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

Para que o JavaScript do lado do cliente assuma o controle com precisão de onde o servidor parou, sem solicitar novamente todos os dados com que o servidor renderizou o HTML, a maioria das soluções de renderização do lado do servidor serializa a resposta das dependências de dados de uma interface como tags de script no documento. Como isso duplica muito HTML, a reidratação pode causar mais problemas do que apenas interatividade atrasada.

Documento HTML que contém interface serializada, dados in-line e um script bundle.js.

O servidor está retornando uma descrição da interface do aplicativo em resposta a uma solicitação de navegação, mas também está retornando os dados de origem usados para compor essa interface e uma cópia completa da implementação da interface, que é inicializada no cliente. A interface não se torna interativa até que bundle.js termine de carregar e executar.

As métricas de performance coletadas de sites reais usando renderização do lado do servidor e reidratação indicam que raramente essa é a melhor opção. O motivo mais importante é o efeito na experiência do usuário, quando uma página parece pronta, mas nenhum dos recursos interativos funciona.

Os efeitos negativos da renderização do lado do cliente no TTI.

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

Renderizar do lado do servidor e reidratar progressivamente

A renderização do lado do servidor passou por vários desenvolvimentos nos últimos anos.

Com a renderização do lado do servidor por streaming, você envia HTML em partes que o navegador pode renderizar progressivamente à medida que são recebidas. Isso pode levar a marcação aos usuários mais rapidamente, acelerando o FCP. No React, os fluxos assíncronos em renderToPipeableStream(), em comparação com renderToString() síncrono, significam que a contrapressão é bem processada.

Também vale a pena considerar a reidratação progressiva (o React já implementou isso). Com essa abordagem, as partes individuais de um aplicativo renderizado no 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, porque permite adiar a atualização do lado do cliente de partes de baixa prioridade da página para evitar que ela bloqueie a linha de execução principal, permitindo que as interações do usuário aconteçam mais cedo depois que ele as inicia.

A reidratação progressiva também pode ajudar a evitar um dos erros mais comuns de reidratação de renderização do lado do servidor: uma árvore DOM renderizada pelo servidor é destruída e reconstruída imediatamente, geralmente porque a renderização inicial síncrona do lado do cliente exigia dados que não estavam prontos, geralmente um Promise que ainda não foi resolvido.

Reidratação parcial

A reidratação parcial tem se mostrado difícil de implementar. Essa abordagem é uma extensão da reidratação progressiva que analisa partes individuais da página (componentes, visualizações ou árvores) e identifica as partes com pouca interatividade ou sem capacidade de reação. Para cada uma dessas partes quase estáticas, o código JavaScript correspondente é transformado em referências inertes e recursos decorativos, reduzindo a pegada do lado do cliente a quase zero.

A abordagem de reidratação parcial tem problemas e concessões próprias. 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ê, considere a renderização trisomórfica. Essa técnica permite usar a renderização do lado do servidor de streaming para navegações iniciais ou não JavaScript e, em seguida, fazer com que o service worker assuma a renderização de HTML para navegações depois de instalado. Isso pode manter os componentes e modelos em cache atualizados e ativar navegações no estilo SPA para renderizar novas visualizações na mesma sessão. Essa abordagem funciona melhor quando você pode compartilhar o mesmo código de modelo e roteamento entre o servidor, a página do cliente e o service worker.

Renderização trisomórfica, demonstrando um navegador e um service worker se comunicando com o servidor.

Considerações sobre SEO

Ao escolher uma estratégia de renderização da Web, as equipes geralmente consideram o impacto do SEO. A renderização do lado do servidor é uma opção popular para oferecer uma experiência "completa" que os rastreadores podem interpretar. Os rastreadores conseguem entender JavaScript, mas geralmente há limitações na forma como eles renderizam. A renderização do lado do cliente pode funcionar, mas geralmente precisa de mais testes e sobrecarga. Mais recentemente, a renderização dinâmica também se tornou uma opção interessante se a arquitetura depender muito do JavaScript do lado do cliente.

Em caso de dúvida, a ferramenta de teste de compatibilidade com dispositivos móveis é uma ótima maneira de verificar se a abordagem escolhida faz o que você espera. Ele mostra uma prévia visual de como qualquer página aparece para o rastreador do Google, o conteúdo HTML serializado que ele encontra depois que o JavaScript é executado e todos os erros encontrados durante a renderização.

A interface do teste de compatibilidade com dispositivos móveis.

Conclusão

Ao decidir uma abordagem de renderização, meça e entenda quais são seus gargalos. Considere se a renderização estática ou do lado do servidor pode ajudar você a chegar lá. Não há problema em enviar principalmente HTML com o mínimo de JavaScript para ter uma experiência interativa. Confira um infográfico útil que mostra o espectro servidor-cliente:

Opções de renderização e suas vantagens e desvantagens.

Créditos {:#credits}

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

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