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 existem muitas maneiras de criar um site.

Nosso entendimento sobre esse espaço é informado pelo nosso trabalho no Chrome, que conversa com grandes sites nos últimos anos. De modo geral, recomendamos que os desenvolvedores usem 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 um entendimento sólido de cada abordagem e uma terminologia consistente para falar sobre elas. As diferenças entre as abordagens de renderização ajudam a ilustrar as compensações da renderização na Web pela perspectiva do desempenho da página.

Terminologia

Primeiro, vamos definir 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.
Reidratação
"Inicialização" de visualizações JavaScript no cliente para que elas reutilizem a árvore DOM e os dados do HTML renderizados pelo servidor.
Pré-renderização
Executar um aplicativo do lado do cliente no momento do build para capturar o estado inicial como HTML estático.

Desempenho

Tempo até o primeiro byte (TTFB)
O tempo entre o clique em um link e o primeiro byte do carregamento 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 à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 foi 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 a busca de dados e a 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 uma FCP rápida. A execução da lógica da página e a renderização no servidor evitam o envio de muito JavaScript para o cliente. Isso ajuda a reduzir o TBT 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 ser executadas mais cedo. Isso faz sentido, porque com a renderização do lado do servidor, você está enviando texto e links para o navegador do usuário. Essa abordagem pode funcionar 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 no servidor e a execução do JS afetando a FCP e o TTI.
FCP e TTI com renderização no servidor.

Com a renderização do lado do servidor, os usuários têm menos chances de esperar que o JavaScript vinculado à CPU seja executado antes de usar o 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, há uma possível desvantagem com essa abordagem: gerar páginas no servidor leva tempo, o que pode aumentar o TTFB da página.

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 de renderização do lado do servidor e do lado do cliente, mas você sempre pode escolher usar a renderização do lado do servidor para algumas páginas e não para outras. Alguns sites adotaram técnicas de renderização híbrida com sucesso. Por exemplo, a Netflix renderiza as páginas de destino relativamente estáticas no servidor, enquanto prefetching do JS para páginas com muita interação, a essas páginas renderizadas pelo cliente uma chance maior de carregamento rápido.

Muitos frameworks, bibliotecas e arquiteturas modernos permitem 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 própria com características e compensações de desempenho muito diferentes. Os usuários do React podem usar APIs DOM do servidor ou soluções criadas com elas, 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 do Vue ou o Nuxt. O Angular tem o Universal. A maioria das soluções mais conhecidas usa alguma forma de hidratação. Portanto, fique atento às abordagens usadas pela sua ferramenta.

Renderização estática

A renderização estática acontece no momento da criação. Essa abordagem oferece um FCP rápido e também um TBT e um INP mais baixos, desde que você limite a quantidade de JS 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. Geralmente, a renderização estática significa produzir um arquivo HTML separado para cada URL com antecedência. Com as respostas HTML geradas com antecedência, é possível implantar renderizações estáticas em vários CDNs para aproveitar o cache de borda.

Diagrama
    mostrando a renderização estática e a execução opcional do JS que afetam a FCP e o TTI.
FCP e TTI com renderização estática.

As soluções de renderização estática têm várias formas e tamanhos. Ferramentas como Gatsby foram criadas para que os desenvolvedores sintam que o aplicativo está sendo renderizado de forma dinâmica, e não gerado como uma etapa de build. Ferramentas de geração de sites estáticos, como 11ty, Jekyll e Metalsmith, acolhem 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 cada URL possível. Isso pode ser desafiador ou até mesmo inviável quando não é possível prever quais serão esses URLs com antecedência ou para sites com um grande número de páginas únicas.

Os usuários do React podem conhecer o Gatsby, o Next.js static export ou o Navi, que facilitam a criação de páginas a partir de 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, tente desativar o JavaScript e carregar a página que você 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 é inativa.

Outro teste útil é usar o controle de taxa de transferência de rede no Chrome DevTools e conferir quantos downloads de JavaScript são feitos antes que uma página se torne 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 aprimoramento progressivo usada na renderização estática.

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

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

Fazer a renderização do lado do servidor "corretamente" pode envolver 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. Você processa ou recria o mesmo app duas vezes, uma no cliente e outra no servidor. A renderização no servidor que mostra o conteúdo mais rápido 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 no servidor produz HTML sob demanda para cada URL, mas pode ser mais lenta do que apenas exibir conteúdo renderizado estático. Se você puder fazer o trabalho adicional, a renderização do lado do servidor e o armazenamento em cache do 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 "ao vivo" 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 uma PWA: é melhor usar o armazenamento em cache do service worker de página inteira ou apenas renderizar partes individuais do conteúdo no servidor?

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, e não no servidor. O resultado efetivo é que mais dados são transmitidos do servidor para o dispositivo do usuário, e isso 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 JavaScript apertado e entregar valor em poucas 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. Você pode fazer com que o analisador funcione mais rápido enviando scripts e dados importantes usando <link rel=preload>. Também recomendamos usar padrões como PRPL para garantir que as navegações iniciais e subsequentes pareçam instantâneas.

Diagrama
    mostrando a renderização do lado do cliente afetando a 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 a INP de uma página. Isso fica especialmente difícil com a adição de novas bibliotecas JavaScript, polyfills e códigos de terceiros, que competem pelo poder de processamento e precisam ser processados antes que o conteúdo de uma página seja renderizado.

As experiências que usam renderização do lado do cliente e dependem de grandes pacotes JavaScript devem considerar o Split de código agressivo para reduzir o TBT e o INP durante o carregamento da página, além do carregamento lento do JavaScript para atender 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 pessoas que criam 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 os service workers, isso pode melhorar drasticamente a performance percebida 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 cliente

A reidratação é uma abordagem que tenta suavizar as compensações entre a renderização do lado do cliente e do servidor fazendo as duas. As solicitações de navegação, como carregamentos de página inteira ou recarregamentos, são tratadas 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 ela pode ter desvantagens consideráveis no 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 a FCP. As páginas renderizadas no 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 "continue" exatamente 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 a interatividade atrasada.

Documento HTML
    contendo interface serializada, dados inline e um script bundle.js
Código duplicado no documento HTML.

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 só fica interativa depois que bundle.js termina de carregar e executar.

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

Diagrama
    mostrando a renderização do cliente afetando negativamente o TTI.
Os efeitos da renderização do lado do cliente no TTI.

No entanto, há esperança para a renderização do lado do servidor com hidratação. No curto prazo, apenas o uso da 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 incremental, progressiva ou parcial pode ser a chave para tornar essa técnica mais viável no futuro.

Transmitir a renderização do servidor e reidratar progressivamente

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

A renderização em streaming do lado do servidor permite enviar HTML em partes que o navegador pode renderizar progressivamente conforme recebido. Isso pode acelerar a marcação para os usuários, acelerando o FCP. No React, os streams assíncronos em renderToPipeableStream(), em comparação com renderToString() síncrono, significam que a contrapressão é bem processada.

A reidratação progressiva também vale a pena considerar, e o React a implementou. Com essa abordagem, 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, 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 rapidamente após a iniciação.

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 síncrona inicial do lado do cliente exigia dados que não estavam totalmente prontos, muitas vezes 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 nenhuma reatividade. 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 hidratação parcial tem problemas e comprometimentos próprios. Ele 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 uma carga de página completa.

Renderização trimórfica

Se os service workers forem uma opção para você, considere a renderização trisomorfa. É uma técnica que permite usar a renderização em streaming do lado do servidor para navegações iniciais ou não JS e, em seguida, fazer com que o service worker assuma a renderização de HTML para navegações após a instalação. Isso pode manter os componentes e modelos em cache atualizados e permitir 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 modelagem e roteamento entre o servidor, a página do cliente e o worker de serviço.

Renderização trimórfica, mostrando um navegador e um worker de serviço se comunicando com o servidor.
Diagrama de como a renderização trisomorfa funciona.

Considerações de SEO

Ao escolher uma estratégia de renderização da Web, as equipes geralmente consideram o impacto do SEO. A renderização no servidor é uma escolha popular para oferecer uma experiência "completa" que os rastreadores podem interpretar. Os rastreadores podem entender o JavaScript, mas geralmente há limitações na renderização. 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 que vale a pena considerar se a arquitetura depende 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 testar se a abordagem escolhida faz o que você espera. Ele mostra uma visualização visual 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.

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

Conclusão

Ao decidir sobre 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 HTML com um mínimo de JavaScript para ter uma experiência interativa. Confira um infográfico útil que mostra o espectro de servidor-cliente:

Infográfico mostrando o espectro de opções descritas neste artigo.
Opções de renderização e as vantagens e desvantagens delas.

Créditos

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

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